The problem
A teammate opens a pull request (PR: a proposed change that others review before it is merged) that adds or changes a Dockerfile. It builds, so it looks fine. But every problem in this chapter - slow builds, huge images, lost SIGTERMs, leaked keys - builds fine too. You need a fast, repeatable way to spot them by reading.
Everything in this chapter compresses into a five-part checklist. Run it top to bottom on any Dockerfile in a PR.
What you need to know already: the whole chapter so far - tags and digests (10.5), the cache and its order (10.13), layers and docker history (10.10), the context and .dockerignore (10.21), multi-stage (10.24), ENTRYPOINT and PID 1 (10.28), base images (10.38), non-root and secrets (10.40), HEALTHCHECK (10.44).
1. The base
FROM node:latest # moving tag, 1.1GB, full toolchain
FROM node:22-slim@sha256:9b1c... # versioned, slim, pinned
- a real version, never
latest(a tag that moves whenever someone pushes) - a runtime base for the final stage (slim / JRE / distroless), a build base only in a build stage
- pinned by digest for anything that ships, with a bot that proposes digest updates so it still gets security fixes
2. Order and the cache
COPY . . # before the install: every edit re-downloads
RUN npm ci
- dependency manifest (
package.json,pom.xml,requirements.txt) copied first, install, then source - no
ARGwith a per-build value (date, SHA, build number) above expensive RUNs - a
.dockerignoreexists and covers.git, dependency dirs, build output, secrets - with**/for nested ones
3. Layers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/* # frees nothing
RUN chown -R app /app # duplicates /app
- update + install + cleanup in one RUN, with
--no-install-recommends - no cleanup-only RUN lines (a later layer can only hide files, not remove bytes)
COPY --chowninstead ofchown -R- package caches off (
--no-cache-dir,apk --no-cache) or in cache mounts ADDonly for local tarballs you mean to extract;COPYotherwise (ADDcan also download URLs - uncached and unverified)
4. What runs, and as whom
ENTRYPOINT java -jar app.jar # sh is PID 1, SIGTERM lost
USER root # or no USER at all
- exec-form ENTRYPOINT/CMD (the JSON list); entrypoint scripts end with
exec "$@" - the process handles SIGTERM (or there is an init such as tini)
- a non-root
USER, preferably numeric (USER 1000): a number can be checked without reading the image's/etc/passwd - writable paths owned by that user, nothing else
EXPOSEmatches what the app really listens on, and the app binds 0.0.0.0
5. Secrets and metadata
ARG NPM_TOKEN
ENV DB_PASSWORD=...
COPY .env .
- no secret in
ARG,ENV, a COPY or a RUN line - build secrets via--mount=type=secret, runtime secrets passed in when the container starts - a
HEALTHCHECKthat uses tools the image actually has (if you use one) - OCI labels (
org.opencontainers.image.source,.revision,.version- the standard label names) so a running image can be traced to a commit
Writing review comments
A useful review comment names the consequence, not just the rule:
- "COPY . . before npm ci: every source change re-downloads ~400MB of deps;
copy package*.json first."
- "Shell-form ENTRYPOINT: sh becomes PID 1, SIGTERM never reaches java, every
deploy waits the grace period and SIGKILLs in-flight requests."
- "ENV DB_PASSWORD: visible to anyone who can pull the image
(docker inspect). Inject at runtime; rotate this one."
Then ask for evidence: docker history before and after, docker images for the size, and a time docker stop for anything touching the entrypoint.
Two linters (programs that read a file and warn about known mistakes) help: BuildKit's own docker build --check . catches a few of these, and hadolint (a Dockerfile linter, also available as an image) catches more:
docker run --rm -i hadolint/hadolint < Dockerfile
(-i keeps standard input open, so the Dockerfile you redirect in reaches the linter.) Worth running both in CI.
What you can now do
- Review a Dockerfile in five passes: base, order, layers, runtime, secrets
- Write a review comment that states the cost, not only the rule
- Ask for the numbers (
docker history,docker images, stop time) as proof