From c895dfd0afae29207ecd9b6a6a8914409a251d50 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Mon, 22 Jun 2026 14:17:30 +0100 Subject: [PATCH 1/8] feat: Add `Containerfile` for application This change uses a multi-stage build to produce a minimal image to run DCR. The image is intended to be run in production in AWS ECS, hence it including only the necessary files. See https://docs.docker.com/build/building/multi-stage/. --- .dockerignore | 2 ++ Containerfile.production | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 .dockerignore create mode 100644 Containerfile.production diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..f06235c460c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/Containerfile.production b/Containerfile.production new file mode 100644 index 00000000000..f7b31ac5069 --- /dev/null +++ b/Containerfile.production @@ -0,0 +1,33 @@ +# TODO use Docker Hardened Images (DHI) for enhanced security. For more information, see https://docs.docker.com/dhi/. +# e.g. FROM dhi.io/node:24-alpine3.23-dev AS base +# e.g. FROM dhi.io/node:24-alpine3.23 AS application + +FROM node:24 AS base +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME/bin:$PATH" +RUN corepack enable +COPY . /app +WORKDIR /app + +# Install dependencies as a separate step to take advantage of Docker's caching. +# Leverage a cache mount to /pnpm/store to speed up subsequent builds. +FROM base AS dependencies +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +WORKDIR /app/dotcom-rendering +ENV PATH="node_modules/.bin:$PATH" +ENV NODE_ENV=production + +# Build the application +FROM dependencies AS builder +RUN webpack --config webpack/webpack.config.js --progress +RUN node scripts/islands/island-descriptions.mjs + +# Finally, create the production image with only the necessary files +FROM node:24 AS application +WORKDIR /app +COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app + +# Expose the port that the application listens on +EXPOSE 9000 + +CMD ["node", "/app/server.js"] From 84b3216062f2664b0ee92e9910d40e6bd59778d6 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Mon, 22 Jun 2026 14:37:34 +0100 Subject: [PATCH 2/8] ci: Add GitHub Workflow to build production container --- .github/workflows/container-production.yml | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/container-production.yml diff --git a/.github/workflows/container-production.yml b/.github/workflows/container-production.yml new file mode 100644 index 00000000000..dd6c45ce65f --- /dev/null +++ b/.github/workflows/container-production.yml @@ -0,0 +1,51 @@ +name: Production container + +# Run this workflow when... +on: + # ...manually invoked + workflow_call: + + # ...a pull request is opened + pull_request: + + # ...`main` changes, e.g. when a PR is merged + push: + branches: + - main + +jobs: + facts: + runs-on: ubuntu-slim + permissions: {} # This job doesn't need any permissions. Explicitly set it to an empty object to avoid inheriting any default permissions of the workflow. + outputs: + branchName: ${{ steps.get-build-facts.outputs.branchName }} + buildNumber: ${{ steps.get-build-facts.outputs.buildNumber }} + commitSha: ${{ steps.get-build-facts.outputs.commitSha }} + steps: + - uses: guardian/actions-build-facts@v0.0.1 + id: get-build-facts + + build-production-image: + runs-on: ubuntu-latest + needs: + - facts + permissions: + contents: read + id-token: write # Required to exchange for AWS credentials using OIDC + outputs: + imageDigest: ${{ steps.publish-image.outputs.imageDigest }} + steps: + - uses: actions/checkout@v6.0.2 + - name: Add commit hash for PRout + working-directory: dotcom-rendering + run: echo 'export const GIT_COMMIT_HASH = "${{ needs.facts.outputs.commitSha }}";' > src/server/prout.ts + - name: Build image + run: docker buildx build -f Containerfile.production -t ${{ github.repository }}:latest . + - name: Publish Image + uses: guardian/actions-publish-image@v0.0.2 + id: publish-image + with: + roleArn: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + branchName: ${{ needs.facts.outputs.branchName }} + buildNumber: ${{ needs.facts.outputs.buildNumber }} + commitSha: ${{ needs.facts.outputs.commitSha }} From f1cf2c1f7491966a1ff10d9b7c56cec487de8f77 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Mon, 22 Jun 2026 14:50:52 +0100 Subject: [PATCH 3/8] refactor: Rename file to gain syntax highlighting on github.com --- .github/workflows/container-production.yml | 2 +- Containerfile.production => Production.dockerfile | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename Containerfile.production => Production.dockerfile (100%) diff --git a/.github/workflows/container-production.yml b/.github/workflows/container-production.yml index dd6c45ce65f..b693ecd05a7 100644 --- a/.github/workflows/container-production.yml +++ b/.github/workflows/container-production.yml @@ -40,7 +40,7 @@ jobs: working-directory: dotcom-rendering run: echo 'export const GIT_COMMIT_HASH = "${{ needs.facts.outputs.commitSha }}";' > src/server/prout.ts - name: Build image - run: docker buildx build -f Containerfile.production -t ${{ github.repository }}:latest . + run: docker buildx build -f Production.dockerfile -t ${{ github.repository }}:latest . - name: Publish Image uses: guardian/actions-publish-image@v0.0.2 id: publish-image diff --git a/Containerfile.production b/Production.dockerfile similarity index 100% rename from Containerfile.production rename to Production.dockerfile From de37f864427617460adcf35aec7e34fc07b4e3fd Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 23 Jun 2026 07:31:19 +0100 Subject: [PATCH 4/8] feat: Switch to hardened Docker image > Docker Hardened Images are built to meet the highest security and compliance standards. They provide a trusted foundation for containerized workloads by incorporating security best practices from the start. > These images are published with near-zero known CVEs, include signed provenance, and come with a complete Software Bill of Materials (SBOM) and VEX metadata. They're designed to secure your software supply chain while fitting seamlessly into existing Docker workflows. The `node:24` image has 6 criticals already! The hardened image does not include `bash`, so switch the `postinstall.sh` script to `sh`. See also: - https://docs.docker.com/dhi/ - https://hub.docker.com/hardened-images/catalog/dhi/node --- Production.dockerfile | 8 ++------ scripts/postinstall.sh | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Production.dockerfile b/Production.dockerfile index f7b31ac5069..29950a2c599 100644 --- a/Production.dockerfile +++ b/Production.dockerfile @@ -1,8 +1,4 @@ -# TODO use Docker Hardened Images (DHI) for enhanced security. For more information, see https://docs.docker.com/dhi/. -# e.g. FROM dhi.io/node:24-alpine3.23-dev AS base -# e.g. FROM dhi.io/node:24-alpine3.23 AS application - -FROM node:24 AS base +FROM dhi.io/node:24-alpine3.23-dev AS base ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME/bin:$PATH" RUN corepack enable @@ -23,7 +19,7 @@ RUN webpack --config webpack/webpack.config.js --progress RUN node scripts/islands/island-descriptions.mjs # Finally, create the production image with only the necessary files -FROM node:24 AS application +FROM dhi.io/node:24-alpine3.23 AS application WORKDIR /app COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app diff --git a/scripts/postinstall.sh b/scripts/postinstall.sh index feede7c03f7..fae29ac04a3 100755 --- a/scripts/postinstall.sh +++ b/scripts/postinstall.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Automatically copy over required settings for vscode if [ ! -f .vscode/settings.json ] ; From 0d9fed0987b035976903c208d14814dc04ea5295 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 23 Jun 2026 07:53:46 +0100 Subject: [PATCH 5/8] ci: Run `container-production` from `cicd` for contiguous build numbers --- .github/workflows/cicd.yml | 8 ++++++++ .github/workflows/container-production.yml | 19 +++++++------------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index ceb24e9681e..4911527128d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -9,6 +9,14 @@ jobs: packages: write uses: ./.github/workflows/container.yml + production-container: + permissions: + contents: read + id-token: write # Required to exchange for AWS credentials using OIDC + uses: ./.github/workflows/container-production.yml + secrets: + GU_RIFF_RAFF_ROLE_ARN: ${{ secrets.GU_RIFF_RAFF_ROLE_ARN }} + prettier: uses: ./.github/workflows/prettier.yml diff --git a/.github/workflows/container-production.yml b/.github/workflows/container-production.yml index b693ecd05a7..0ecf3611a2f 100644 --- a/.github/workflows/container-production.yml +++ b/.github/workflows/container-production.yml @@ -1,18 +1,13 @@ name: Production container - -# Run this workflow when... on: - # ...manually invoked workflow_call: - - # ...a pull request is opened - pull_request: - - # ...`main` changes, e.g. when a PR is merged - push: - branches: - - main - + secrets: + GU_RIFF_RAFF_ROLE_ARN: + required: true + outputs: + imageDigest: + description: 'The digest of the generated container image' + value: ${{ jobs.build-production-image.outputs.imageDigest }} jobs: facts: runs-on: ubuntu-slim From ef60bf12460c107fd43c7dc95c0d4567e17a9693 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 23 Jun 2026 08:29:13 +0100 Subject: [PATCH 6/8] fix: Disable Log4js in production container Writing logs to disk is non-standard. It is more common to log to stdout/stderr and have a sidecar process the logs. Additionally, within the hardened image, we don't have `mkdir` permissions. --- Production.dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Production.dockerfile b/Production.dockerfile index 29950a2c599..cf88419eeb1 100644 --- a/Production.dockerfile +++ b/Production.dockerfile @@ -23,6 +23,10 @@ FROM dhi.io/node:24-alpine3.23 AS application WORKDIR /app COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app +# Disable logging with Log4js as console logs will be forwarded to Central ELK with a sidecar +# TODO Maintain metrics +ENV DISABLE_LOGGING_AND_METRICS=true + # Expose the port that the application listens on EXPOSE 9000 From 4455867b84fcbc7c6b3a991f104df4e38bab21ed Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 23 Jun 2026 13:42:46 +0100 Subject: [PATCH 7/8] fix: Add `.git` and `.github` to ignored docker files Suggested by copilot, this will decrease build times. --- .dockerignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.dockerignore b/.dockerignore index f06235c460c..de86122ce24 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,4 @@ +.git +.github node_modules dist From 63e735eaa7325a7581451e5d61e736f17c348ed6 Mon Sep 17 00:00:00 2001 From: akash1810 Date: Tue, 23 Jun 2026 13:45:38 +0100 Subject: [PATCH 8/8] fix(docker): Set `NODE_ENV` for the application --- Production.dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Production.dockerfile b/Production.dockerfile index cf88419eeb1..c31c688555c 100644 --- a/Production.dockerfile +++ b/Production.dockerfile @@ -26,6 +26,7 @@ COPY --from=builder --chown=node:node /app/dotcom-rendering/dist /app # Disable logging with Log4js as console logs will be forwarded to Central ELK with a sidecar # TODO Maintain metrics ENV DISABLE_LOGGING_AND_METRICS=true +ENV NODE_ENV=production # Expose the port that the application listens on EXPOSE 9000