diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..98ad238a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +node_modules +tmp +log +.git +.github +*.log +.env +dist +tests diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index b1a3e647..f66b7ba4 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -4,16 +4,16 @@ on: workflow_dispatch: push: branches: - - main + - master jobs: flow: name: Highsoft Flow - uses: highsoft-corp/hs-platform-workflows/.github/workflows/flow_docker.yml@feat/multi-env-builds + uses: ./.github/workflows/flow_docker.yml secrets: inherit with: registry: ghcr.io folder_name: . image_name: ${{ github.repository }} - use_image_per_environment: false - platforms: linux/amd64 \ No newline at end of file + platforms: linux/amd64 + deploy_branch: master diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 859c4425..5eb3ef6a 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/docker_build.yml b/.github/workflows/docker_build.yml new file mode 100644 index 00000000..09f55522 --- /dev/null +++ b/.github/workflows/docker_build.yml @@ -0,0 +1,116 @@ +name: Build Docker Image + +on: + workflow_call: + inputs: + version: + description: "The version number" + default: "1.0.0" + required: false + type: string + registry: + description: "The Docker Registry" + default: "ghcr.io" + required: false + type: string + image_name: + description: "The Docker image name" + default: ${{ github.repository }} + required: false + type: string + folder_name: + description: "The folder where the Dockerfile is located" + default: . + required: false + type: string + user_name: + description: "The name of the user to login to the registry" + default: ${{ github.actor }} + required: false + type: string + run_number: + description: "The workflow run number" + default: ${{ github.run_number }} + required: false + type: string + environment: + description: "The GitHub environment to use (for environment-specific secrets/variables)" + required: false + type: string + platforms: + description: "The platforms to build for" + required: false + type: string + default: linux/amd64,linux/arm64 + + outputs: + build_image: + description: "The build image" + value: ${{ jobs.build.outputs.build_image }} + build_image_tag: + description: "The build image tag" + value: ${{ jobs.build.outputs.build_image_tag }} + version: + description: "The new version number" + value: ${{ jobs.build.outputs.version }} + +jobs: + build: + name: ${{ inputs.environment && format('Build {0} Image', inputs.environment) || 'Build Image' }} + runs-on: ubuntu-latest + environment: ${{ inputs.environment }} + + permissions: + contents: read + packages: write + attestations: write + + outputs: + build_image: ${{ steps.output.outputs.build_image }} + build_image_tag: ${{ steps.output.outputs.build_image_tag }} + version: ${{ steps.output.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.user_name }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ inputs.registry }}/${{ inputs.image_name }} + + - name: Build and publish image + id: push + uses: docker/build-push-action@v7.0.0 + with: + context: ${{ inputs.folder_name }} + push: true + tags: ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }}${{ inputs.environment && format('-{0}', inputs.environment) || '' }}-build.${{ inputs.run_number }} + labels: ${{ steps.meta.outputs.labels }} + platforms: ${{ inputs.platforms }} + + - name: Store outputs + id: output + run: | + echo "build_image=${{ inputs.registry }}/${{ inputs.image_name }}" >> "$GITHUB_OUTPUT" + echo "build_image_tag=${{ inputs.version }}${{ inputs.environment && format('-{0}', inputs.environment) || '' }}-build.${{ inputs.run_number }}" >> "$GITHUB_OUTPUT" + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" + + - name: Generate summary + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY; + echo "Build Image: ${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.version }}${{ inputs.environment && format('-{0}', inputs.environment) || '' }}-build.${{ inputs.run_number }}" >> $GITHUB_STEP_SUMMARY; diff --git a/.github/workflows/docker_deploy.yml b/.github/workflows/docker_deploy.yml new file mode 100644 index 00000000..7814c009 --- /dev/null +++ b/.github/workflows/docker_deploy.yml @@ -0,0 +1,74 @@ +name: Deploy Docker Image + +on: + workflow_call: + inputs: + version: + description: "The version number" + required: true + type: string + registry: + description: "The Docker Registry" + default: "ghcr.io" + required: false + type: string + image_name: + description: "The Docker image name" + default: ${{ github.repository }} + required: false + type: string + build_image: + description: "The Docker Build Image (repository) to deploy" + required: true + type: string + build_image_tag: + description: "The Docker Build Image Tag to retag from" + required: true + type: string + deploy_tag: + description: "The target tag to deploy as" + required: true + type: string + environment: + description: "The GitHub environment to deploy to" + required: true + type: string + platforms: + description: "The platforms to deploy" + required: false + type: string + default: linux/amd64,linux/arm64 + + outputs: + image: + description: "The deployed image" + value: ${{ jobs.deploy.outputs.image }} + +jobs: + deploy: + name: ${{ inputs.environment && format('Deploy {0} Image', inputs.environment) || 'Deploy Image' }} + runs-on: ubuntu-latest + + environment: ${{ inputs.environment }} + permissions: + contents: read + packages: write + attestations: write + + outputs: + image: ${{ steps.output.outputs.image }} + + steps: + - name: Deploy to ${{ inputs.environment }} + uses: shrink/actions-docker-registry-tag@v4 + with: + registry: ${{ inputs.registry }} + repository: ${{ inputs.build_image }} + target: ${{ inputs.build_image_tag }} + tags: | + ${{ inputs.deploy_tag }} + + - name: Store outputs + id: output + run: | + echo "image=${{ inputs.registry }}/${{ inputs.image_name }}:${{ inputs.deploy_tag }}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/eslint-check.yml b/.github/workflows/eslint-check.yml index 62fff57d..4bf3adf6 100644 --- a/.github/workflows/eslint-check.yml +++ b/.github/workflows/eslint-check.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/.github/workflows/flow_docker.yml b/.github/workflows/flow_docker.yml new file mode 100644 index 00000000..943f7049 --- /dev/null +++ b/.github/workflows/flow_docker.yml @@ -0,0 +1,126 @@ +name: Build and Deploy Docker Image + +on: + workflow_call: + inputs: + registry: + description: "The Docker Registry" + default: "ghcr.io" + required: false + type: string + image_name: + description: "The Docker image name" + default: ${{ github.repository }} + required: false + type: string + folder_name: + description: "The folder where the Dockerfile is located" + default: . + required: false + type: string + deploy_branch: + description: "The branch that will be deployed" + default: ${{ github.event.repository.default_branch }} + required: false + type: string + platforms: + description: "The platforms to build for" + required: false + type: string + default: linux/amd64 + use_image_per_environment: + description: "Whether to create one image for each environment. Needed for statically generated webapps like NextJS" + default: false + required: false + type: boolean + +jobs: + version: + name: Read Version + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Read version from VERSION file + id: version + working-directory: ${{ inputs.folder_name }} + run: | + VERSION=$(cat VERSION) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "## Version" >> $GITHUB_STEP_SUMMARY + echo "$VERSION" >> $GITHUB_STEP_SUMMARY + + build: + name: Build Image + uses: ./.github/workflows/docker_build.yml + if: ${{ !inputs.use_image_per_environment }} + secrets: inherit + needs: version + with: + registry: ${{ inputs.registry }} + image_name: ${{ inputs.image_name }} + folder_name: ${{ inputs.folder_name }} + version: ${{ needs.version.outputs.version }} + run_number: ${{ github.run_number }} + user_name: ${{ github.actor }} + platforms: ${{ inputs.platforms }} + + # Deploy to Development + deploy-dev: + name: Deploy Development Image + uses: ./.github/workflows/docker_deploy.yml + secrets: inherit + if: ${{ !failure() && !cancelled() && github.ref_name == inputs.deploy_branch }} + needs: + - version + - build + with: + version: ${{ needs.version.outputs.version }} + registry: ${{ inputs.registry }} + image_name: ${{ inputs.image_name }} + build_image: ${{ needs.build.outputs.build_image }} + build_image_tag: ${{ needs.build.outputs.build_image_tag }} + deploy_tag: ${{ needs.version.outputs.version }}-dev + platforms: ${{ inputs.platforms }} + environment: development + + # Deploy to Staging + deploy-staging: + name: Deploy Staging Image + uses: ./.github/workflows/docker_deploy.yml + secrets: inherit + if: ${{ !failure() && !cancelled() && github.ref_name == inputs.deploy_branch }} + needs: + - version + - build + with: + version: ${{ needs.version.outputs.version }} + registry: ${{ inputs.registry }} + image_name: ${{ inputs.image_name }} + build_image: ${{ needs.build.outputs.build_image }} + build_image_tag: ${{ needs.build.outputs.build_image_tag }} + deploy_tag: ${{ needs.version.outputs.version }}-staging + environment: staging + platforms: ${{ inputs.platforms }} + + # Deploy to Production + deploy-prod: + name: Deploy Production Image + uses: ./.github/workflows/docker_deploy.yml + secrets: inherit + if: ${{ !failure() && !cancelled() && github.ref_name == inputs.deploy_branch }} + needs: + - version + - build + with: + version: ${{ needs.version.outputs.version }} + registry: ${{ inputs.registry }} + image_name: ${{ inputs.image_name }} + build_image: ${{ needs.build.outputs.build_image }} + build_image_tag: ${{ needs.build.outputs.build_image_tag }} + deploy_tag: ${{ needs.version.outputs.version }} + environment: production + platforms: ${{ inputs.platforms }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index c23e98f8..e8142dbe 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout Repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v4 diff --git a/.versionrc b/.versionrc new file mode 100644 index 00000000..69a24070 --- /dev/null +++ b/.versionrc @@ -0,0 +1,14 @@ +{ + "packageFiles": [ + { + "filename": "VERSION", + "type": "plain-text" + } + ], + "bumpFiles": [ + { + "filename": "VERSION", + "type": "plain-text" + } + ] +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..b8203353 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +FROM node:24-bookworm-slim + +ENV NODE_ENV=production \ + PUPPETEER_SKIP_DOWNLOAD=true \ + PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \ + PUPPETEER_TEMP_DIR=/tmp/hc-export + +# Install browser and fonts +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + fonts-liberation \ + fonts-noto \ + fonts-noto-color-emoji \ + fonts-noto-cjk \ + texlive-fonts-recommended \ + texlive-fonts-extra \ + cm-super \ + fontconfig \ + ca-certificates \ + curl \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +# Install Highcharts export server fonts +RUN curl -fsSL https://assets.highcharts.com/export-srv/fonts.zip -o /tmp/fonts.zip \ + && mkdir -p /usr/share/fonts/highcharts \ + && unzip -o /tmp/fonts.zip -d /usr/share/fonts/highcharts \ + && rm /tmp/fonts.zip \ + && fc-cache -f + +WORKDIR /app + +# Install deps +COPY package*.json ./ +RUN npm ci --omit=dev --ignore-scripts + +COPY . . + +# Set up temp folder +RUN mkdir -p /tmp/hc-export && chown -R node:node /app /tmp/hc-export + +# Run as unprivileged node user +USER node + +EXPOSE 7801 + +CMD ["node", "./bin/cli.js", "--enableServer", "1", "--loadConfig", "./docker/config.json"] diff --git a/README.md b/README.md index bae859d9..a5839cd1 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,42 @@ To use the Export Server, simply run the following command with the correct argu highcharts-export-server ``` +# Running with Docker + +Build and run: + +``` +docker build -t highcharts-export-server . +docker run --rm -p 7801:7801 highcharts-export-server +``` + +Or with Docker Compose: + +``` +docker compose up --build +``` + +The server listens on port `7801`. Test it with `curl http://localhost:7801/health`. + +Settings can be overridden at runtime with environment variables, which take +precedence over the loaded config file. For example, to allow a few concurrent +workers or change the port: + +``` +docker run --rm -p 8080:8080 \ + -e POOL_MAX_WORKERS=4 \ + -e SERVER_PORT=8080 \ + highcharts-export-server +``` + +By default the server fetches Highcharts scripts from the CDN on first export +(and caches them), so the container needs outbound network access on startup. +For fully offline operation, use the bundled `highcharts` dependency instead: + +``` +docker run --rm -p 7801:7801 -e HIGHCHARTS_USE_NPM=true highcharts-export-server +``` + # Configuration There are four main ways of loading configurations: diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..acf69b48 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +5.1.0 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..60aaef79 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + export-server: + build: . + ports: + - "7801:7801" + restart: unless-stopped diff --git a/docker/config.json b/docker/config.json new file mode 100644 index 00000000..30d01d90 --- /dev/null +++ b/docker/config.json @@ -0,0 +1,65 @@ +{ + "pool": { + "minWorkers": 1, + "maxWorkers": 1 + }, + "other": { + "browserShellMode": false + }, + "logging": { + "toFile": false, + "toConsole": true + }, + "puppeteer": { + "args": [ + "--allow-running-insecure-content", + "--ash-no-nudges", + "--autoplay-policy=user-gesture-required", + "--block-new-web-contents", + "--disable-accelerated-2d-canvas", + "--disable-background-networking", + "--disable-background-timer-throttling", + "--disable-backgrounding-occluded-windows", + "--disable-breakpad", + "--disable-checker-imaging", + "--disable-client-side-phishing-detection", + "--disable-component-extensions-with-background-pages", + "--disable-component-update", + "--disable-default-apps", + "--disable-dev-shm-usage", + "--disable-domain-reliability", + "--disable-extensions", + "--disable-features=CalculateNativeWinOcclusion,InterestFeedContentSuggestions,WebOTP", + "--disable-hang-monitor", + "--disable-ipc-flooding-protection", + "--disable-logging", + "--disable-notifications", + "--disable-offer-store-unmasked-wallet-cards", + "--disable-popup-blocking", + "--disable-print-preview", + "--disable-prompt-on-repost", + "--disable-renderer-backgrounding", + "--disable-search-engine-choice-screen", + "--disable-session-crashed-bubble", + "--disable-setuid-sandbox", + "--disable-site-isolation-trials", + "--disable-speech-api", + "--disable-sync", + "--enable-unsafe-webgpu", + "--hide-crash-restore-bubble", + "--hide-scrollbars", + "--metrics-recording-only", + "--mute-audio", + "--no-default-browser-check", + "--no-first-run", + "--no-pings", + "--pipe", + "--no-startup-window", + "--password-store=basic", + "--process-per-tab", + "--use-mock-keychain", + "--no-sandbox", + "--no-zygote" + ] + } +} diff --git a/package-lock.json b/package-lock.json index 45c36f7c..4cbefcf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "highcharts-export-server", - "version": "5.0.0", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "highcharts-export-server", - "version": "5.0.0", + "version": "5.1.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -90,6 +90,7 @@ "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.7", @@ -1724,6 +1725,7 @@ "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2386,6 +2388,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -3214,7 +3217,8 @@ "version": "0.0.1312386", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/diff-sequences": { "version": "29.6.3", @@ -3545,6 +3549,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3601,6 +3606,7 @@ "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7191,6 +7197,7 @@ "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7723,6 +7730,7 @@ "integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.6" },