Build verifiable artifacts

Harden container images

Use trusted minimal bases, pin image identity, build without secrets, scan final layers, and run with reduced runtime privileges.

A container image is a release artifact with its own operating-system packages and history. Review the final image, not only the Dockerfile.

Start with the base. Use supported base images, pin a digest where stability matters, and rebuild for security updates:

FROM node:22-alpine@sha256:9fcc1a6da2b9eaa4d8d8e2b6f26b2fcd8f9c1a3e5d4b7a8c2f1e0d9c8b7a6f5e

A tag like node:22-alpine is mutable. The digest is not. Pinning means base updates arrive through a reviewed pull request instead of silently on the next build.

Never bake secrets into layers

Here is the classic mistake. A Dockerfile copies .npmrc, installs private packages, and deletes the file in the next instruction. The credential remains recoverable from the earlier layer, because each instruction creates a layer and deletion only masks the file in later ones.

Use a BuildKit secret mount instead. The file exists during the one instruction and lands in no layer:

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci --omit=dev
docker build --secret id=npmrc,src=.npmrc -t ghcr.io/acme/api:1.4.2 .

Verify it worked by inspecting the layer history:

docker history --no-trunc ghcr.io/acme/api:1.4.2

No layer should show the credential file being copied in.

Ship less, run with less

Use a multi-stage build so compilers, dev dependencies, and package caches stay in the build stage and out of the runtime stage. Then run as a non-root user when the application supports it. The official Node images include one:

USER node

A container compromise now starts without root inside the container.

One caveat: a minimal image reduces packages and findings, but it can make diagnosis harder. Keep a separate debugging path, an ephemeral debug container or a debug image variant, instead of shipping compilers and shells in every production image.

Try this on your own project: inspect one final image for layers, packages, configured user, entry point, and embedded secrets, and save the results. Rebuild it with a secret mount and a separate runtime stage, then prove the credential is absent. Start the container as its configured non-root user and test one operation that should fail without elevated privileges.

Lesson completed