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.
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