Tools · Containers and Kubernetes security · 12 / 20
Dockerfile security best practices: non-root users, multi-stage builds, and no secrets in layers
Rewrite a typical Dockerfile to pin its base, drop build tools with a multi-stage build, run as a numeric non-root user, and keep secrets out of image history.
What goes wrong
Common issues: an unpinned latest base, compilers and package managers left in the final image, the app running as root, secrets passed with ARG or copied in and deleted in a later layer, and a broad COPY . that pulls in .git or .env files. Every layer is kept in the image, so a file deleted later is still recoverable from the earlier layer.
A better Dockerfile
Use a multi-stage build so the final image contains only the runtime and the built app. Pin the base by tag and ideally by digest. Create a user with a numeric UID and switch to it. Use BuildKit secret mounts for credentials needed during build, so they never land in a layer. Add a .dockerignore.
# syntax=docker/dockerfile:1 FROM node:22-bookworm-slim AS build WORKDIR /src COPY package*.json ./ RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci COPY . . RUN npm run build && npm prune --omit=dev FROM gcr.io/distroless/nodejs22-debian12 WORKDIR /app COPY --from=build /src/dist ./dist COPY --from=build /src/node_modules ./node_modules USER 10001 CMD ["dist/server.js"]
Lab: find a secret in history
Build a deliberately bad image that copies a fake token file and deletes it in the next RUN. Run docker history and then save the image and list each layer’s files: the token is still there. Rebuild using a secret mount and confirm the file is absent from every layer. Use only fake values.
docker history --no-trunc bad-image:lab docker save bad-image:lab -o bad.tar && mkdir bad && tar -xf bad.tar -C bad find bad -name '*.tar' -o -name 'layer.tar' | head trivy image --scanners secret bad-image:lab DOCKER_BUILDKIT=1 docker build --secret id=npmrc,src=$HOME/.npmrc -t good-image:lab .
Lint it
Hadolint checks Dockerfiles for these issues and many more, such as missing version pins for apt packages. trivy config and Docker Scout also report Dockerfile misconfigurations. Add one of them to CI next to the image scan.
docker run --rm -i hadolint/hadolint < Dockerfile
Diagram
Typical Dockerfile versus hardened Dockerfile
Each change removes something an attacker could use.
Base. node:latest
Contents. Compilers, npm cache, source, .git
User. root
Secrets. ARG TOKEN or copied file
Base. Pinned slim or distroless
Contents. Runtime and built app only
User. USER 10001
Secrets. BuildKit secret mount, never in a layer
Check yourself
1. A Dockerfile copies a key file and deletes it in the next instruction. Is the key gone from the image?
2. Which technique keeps compilers out of the final image?
3. How should a build-time credential be provided?