Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/auto-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
latest_alpine_version=$(jq -r '.[] | select(.tags[] == "latest") | .alpineVersion' image-matrix.json)
sed -i "s/ARG BASE_VERSION=.*/ARG BASE_VERSION=${latest_node_version}-alpine${latest_alpine_version}/" Dockerfile
sed -i "s/ARG DEFRA_VERSION=.*/ARG DEFRA_VERSION=${new_defra_version}/" Dockerfile
sed -i "s/ARG BASE_VERSION=.*/ARG BASE_VERSION=${new_defra_version}-node${latest_node_version}/" examples/Dockerfile.web examples/Dockerfile.service
sed -i "s/ARG PARENT_VERSION=.*/ARG PARENT_VERSION=${new_defra_version}-node${latest_node_version}/" examples/Dockerfile
echo "Updated Dockerfile and examples"

- name: Prepare pull request details
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Set default values for build arguments
ARG DEFRA_VERSION=3.1.3
ARG DEFRA_VERSION=3.1.4
ARG BASE_VERSION=24.19.0-alpine3.24
ARG NPM_VERSION=12.0.2

Expand All @@ -16,13 +16,13 @@ ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH=$PATH:/home/node/.npm-global/bin
ENV NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/internal-ca.crt

RUN apk add --no-cache tini ca-certificates

# Upgrade the bundled npm CLI to a pinned version to clear vulnerabilities in the
# libraries npm ships with. Target /usr/local explicitly so the base image's own npm
# is replaced rather than a second copy installed under NPM_CONFIG_PREFIX.
# NPM_VERSION is kept current by the auto-update workflow.
RUN npm install -g --prefix=/usr/local npm@${NPM_VERSION} && npm cache clean --force
RUN apk add --no-cache tini ca-certificates \
&& npm install -g --ignore-scripts --prefix=/usr/local "npm@${NPM_VERSION}" \
&& npm cache clean --force

# Install Internal CA certificate for firewall and Zscaler proxy
COPY certificates/internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
Expand Down
2 changes: 1 addition & 1 deletion JOB.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
DEFRA_VERSION=3.1.3
DEFRA_VERSION=3.1.4
IMAGE_NAME=node
NPM_VERSION=12.0.2
36 changes: 30 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,9 @@ Two parent images are created for each version:

It is recommended that services use [multi-stage builds](https://docs.docker.com/develop/develop-images/multistage-build) to produce production and development images, each extending the appropriate parent, from a single Dockerfile.

### Example files
### Example file

[Examples](https://github.com/DEFRA/defra-docker-node/tree/main/examples) are provided to show how parent images can be extended for different types of services. These should be a good starting point for building Node services conforming to Defra standards.

`Dockerfile.web` - This is an example web project, that requires a build step to create some static files that are used by the web front end.

`Dockerfile.service` - This is an example project that doesn't expose any external ports (a message based service). There is also no build step in this Dockerfile.
[`examples/Dockerfile`](https://github.com/DEFRA/defra-docker-node/tree/main/examples) shows how the parent images can be extended for a Node service, with `development` and `production` targets. There's no separate stage for running tests — run `npm test` directly, on the host or in CI, against the same source tree used by the `development` stage. If your service has no build step, delete the `RUN npm run build` lines and copy from the `development` stage directly in the production stage instead of from `production-build`.

## Supported Node.js versions

Expand Down Expand Up @@ -64,6 +60,34 @@ A build is only blocked by vulnerabilities that have a fix available, so unpatch

For more details see [Image Scanning](IMAGE_SCANNING.md)

## Software Bill of Materials (SBOM)

On every push to `main`, each production image variant has an SBOM generated from its actual container contents using [Syft](https://github.com/anchore/syft) (via [anchore/sbom-action](https://github.com/anchore/sbom-action)), which is:

- submitted to this repository's Dependency graph, so vulnerable OS packages and runtime dependencies show up alongside Dependabot alerts, and
- uploaded as a downloadable workflow artifact for that run.

The image pushed to Docker Hub also carries the same SBOM as a build attestation (`docker buildx build --sbom=true`). You can inspect it directly from the published image without pulling it:

```
docker buildx imagetools inspect defradigital/node:<tag> --format '{{json (index .SBOM "linux/amd64").SPDX}}'
```

### Retiring a version

Each supported Node major version submits its SBOM to the Dependency Graph under its own
correlator (`docker-image-node-<major>`), and GitHub only ever shows the *latest* submission
for a given correlator. So if a version is simply deleted from [image-matrix.json](image-matrix.json)
once it drops out of LTS, nothing ever submits again for that correlator, and the Dependency
Graph (and any Dependabot alerts derived from it) would keep showing that version's packages
forever, frozen at whatever they were on its last build.

To retire a version cleanly, run [`scripts/retire-version.sh`](scripts/retire-version.sh) with
the major version, e.g. `./scripts/retire-version.sh 22`. It removes the version from
`image-matrix.json` and the table above, and submits an empty snapshot for that version's
correlator to clear it from the Dependency Graph. Review the resulting diff, then commit it and
open a PR as normal. Requires `jq` and an authenticated `gh` CLI.

## Automated version updates

The [auto-update](/.github/workflows/auto-update.yml) workflow runs nightly. It checks for new releases of Node.js (and their Alpine images) and of the npm CLI, and when it finds one it opens a pull request that bumps the affected versions across the [image-matrix.json](image-matrix.json), [JOB.env](JOB.env), [Dockerfile](Dockerfile), [README.md](README.md) and the [examples](examples).
Expand Down
48 changes: 48 additions & 0 deletions examples/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
ARG PARENT_VERSION=3.1.4-node24.19.0
ARG PORT=3000

FROM defradigital/node-development:$PARENT_VERSION AS development

ENV TZ="Europe/London"

ARG PORT
ENV PORT=${PORT}
EXPOSE ${PORT} 9229

COPY --chown=node:node package*.json ./
RUN npm ci --ignore-scripts
COPY --chown=node:node src/ ./src/

RUN npm run build
CMD [ "npm", "run", "start:watch" ]

# Re-runs the build with NODE_ENV=production set, for an optimised production bundle.
FROM development AS production-build
ENV NODE_ENV=production
RUN npm run build

# Production stage: fresh production-only install, read-only application files.
FROM defradigital/node:$PARENT_VERSION AS production

ENV TZ="Europe/London"

ARG PORT
EXPOSE ${PORT}

# Switch to root temporarily to install production dependencies and lock down file permissions.
USER root

# Copy application artifacts and assign root ownership to prevent modification by other users.
COPY --from=production-build --chown=root:root /home/node/package*.json ./
COPY --from=production-build --chown=root:root /home/node/src/ ./src/

# Install node modules, remove write permissions, then drop back to the unprivileged user.
RUN npm ci --ignore-scripts --omit=dev && chmod -R a-w /home/node
USER node

# This is the command that is run for the production service. The parent image has an ENTRYPOINT that uses a lightweight
# init program "tini" that handles signals. As long as we don't override the ENTRYPOINT the "tini" routine will handle signals and
# orphaned processes
CMD [ "node", "src/index" ]


45 changes: 0 additions & 45 deletions examples/Dockerfile.service

This file was deleted.

62 changes: 0 additions & 62 deletions examples/Dockerfile.web

This file was deleted.

67 changes: 67 additions & 0 deletions scripts/retire-version.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail

# Retires a Node.js major version once it drops out of LTS: removes it from
# image-matrix.json and README.md, and clears its entry from the GitHub
# Dependency Graph (which would otherwise keep showing that version's
# packages forever, since nothing else would ever resubmit for it).
#
# Usage: scripts/retire-version.sh <major-node-version>
# Example: scripts/retire-version.sh 22

if [[ $# -ne 1 ]]; then
echo "Usage: $0 <major-node-version>" >&2
exit 1
fi
if ! command -v jq >/dev/null || ! command -v gh >/dev/null; then
echo "This script requires both jq and the GitHub CLI (gh, authenticated)." >&2
exit 1
fi

MAJOR="$1"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MATRIX_FILE="$REPO_ROOT/image-matrix.json"
README_FILE="$REPO_ROOT/README.md"
REPO_SLUG="$(gh repo view --json nameWithOwner --jq .nameWithOwner)"

FULL_VERSION=$(jq -r --arg major "$MAJOR" '.[] | select(.nodeVersion | startswith($major + ".")) | .nodeVersion' "$MATRIX_FILE")
if [[ -z "$FULL_VERSION" ]]; then
echo "No entry for Node $MAJOR found in $MATRIX_FILE" >&2
exit 1
fi

echo "Removing Node $FULL_VERSION from image-matrix.json"
jq --arg major "$MAJOR" '[.[] | select((.nodeVersion | startswith($major + ".")) | not)]' "$MATRIX_FILE" > "$MATRIX_FILE.tmp"
mv "$MATRIX_FILE.tmp" "$MATRIX_FILE"

echo "Removing the $FULL_VERSION row from README.md"
grep -v "| ${FULL_VERSION} " "$README_FILE" > "$README_FILE.tmp"
mv "$README_FILE.tmp" "$README_FILE"

CORRELATOR="docker-image-node-$MAJOR"
echo "Clearing Dependency Graph entry for correlator $CORRELATOR"
# job.correlator + detector.name (not detector.version) are what GitHub uses to
# decide which snapshot is "current"; an empty manifest set clears that entry.
# detector.name must match what anchore/sbom-action's syft integration submits.
gh api "repos/$REPO_SLUG/dependency-graph/snapshots" --input - <<EOF
{
"version": 0,
"sha": "$(git -C "$REPO_ROOT" rev-parse origin/main)",
"ref": "refs/heads/main",
"job": {
"correlator": "$CORRELATOR",
"id": "retire-version-script"
},
"detector": {
"name": "syft",
"version": "0.0.0",
"url": "https://github.com/anchore/syft"
},
"scanned": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"manifests": {}
}
EOF

echo
echo "Done. Review the diff, commit image-matrix.json and README.md, and open a PR."
echo "Verify in GitHub: Insights > Dependency graph > Dependencies that the Node $FULL_VERSION packages are gone."
Loading