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