Dockerfiles Best Practices
This is a working checklist for production Dockerfiles - fast to build, small to ship, and safe to run. Apply the groups in order; each practice states what to do and why it matters.
Search across all documentation pages
This is a working checklist for production Dockerfiles - fast to build, small to ship, and safe to run. Apply the groups in order; each practice states what to do and why it matters.
Treat this as a review gate for any Dockerfile heading to production. Skim the group headers, then check each bold practice against your file.
Not every item fits every app, but you should be able to justify each one you skip. Pair it with a linter such as hadolint in CI so regressions are caught automatically.
latest. latest moves under you and makes builds non-reproducible; use node:22-slim over node:latest.FROM node:22-slim@sha256:... guarantees the exact bytes even if the tag is re-pushed.COPY package.json then install, then COPY . ., so code edits do not bust the dependency cache.npm ci, pip install -r with a lockfile, or pinned apt versions keep builds repeatable..dockerignore. Excluding .git, node_modules, and secrets shrinks the context and protects the cache and the image.RUN commands. Combine apt-get update && install && rm -rf /var/lib/apt/lists/* in one layer so caches never persist.COPY --from only artifacts, so toolchains never reach production.RUN --mount=type=cache speeds installs without baking the cache into a layer.USER so a compromised process is unprivileged; it also satisfies runAsNonRoot.--mount=type=secret, not ARG or ENV, since args and env persist in image history.COPY --chown and write only to mounted volumes so readOnlyRootFilesystem works.CMD/ENTRYPOINT. ["node", "server.js"] makes the process PID 1 and delivers signals for clean shutdown.WORKDIR. It avoids surprises from relative paths and never relies on a non-persistent RUN cd.EXPOSE. It signals intent to readers and tooling, even though publishing happens at run time.SIGTERM; add an init like tini only if the process cannot reap children.# syntax=docker/dockerfile:1 to unlock BuildKit mounts and stable parsing.linux/amd64 and linux/arm64 for Apple Silicon and Graviton via docker buildx.hadolint catches unpinned versions, root users, and shell-form pitfalls before review.apt-get/curl makes builds drift over time.Rebuild from a clean cache and confirm the image is small, non-root, and free of high-severity CVEs. Run docker history and a scanner as the final check.
The finished Dockerfile should build fast on a warm cache, produce a minimal runtime image, and pass your linter and image policy without manual exceptions.
Record the final image digest in your deployment manifests rather than a floating tag, so what you tested is exactly what runs. Promote that digest through environments instead of rebuilding per stage.
Finally, confirm the runtime posture end to end: the container starts as a non-root UID, writes only to mounted volumes, and stops cleanly on SIGTERM. A Dockerfile that builds well but cannot shut down gracefully still causes rollout pain.
Should I always pin base image digests? For production and reproducible builds, yes. Digests guarantee identical bytes; combine with scheduled rebuilds to still get security updates.
Why chain RUN commands? Each RUN is a layer. Chaining lets you install and clean up in one layer so caches and temp files never ship.
Is Alpine always the best minimal base? No. Alpine uses musl, which breaks some glibc binaries and native modules. Distroless or slim can be safer for those.
How do I keep secrets out of the image? Use BuildKit secret mounts. Never pass tokens via ARG or ENV, since both persist in image history.
Do I need multi-arch images? Only if your image runs on more than one CPU architecture, such as Graviton nodes plus x86, or developers on Apple Silicon.
What runs the container in Kubernetes? containerd via the CRI runs your pods on nodes. Docker builds the image; it is not the in-cluster runtime.
Should I set requests, limits, and probes in the Dockerfile? No. Those belong to the Kubernetes workload spec, not the image. The Dockerfile's job is a correct, minimal, non-root image; the pod spec sets resources and health checks.
Do I need a linter if I follow this list by hand? A linter catches regressions the moment they are introduced. Manual review drifts over time, so run hadolint in CI as a backstop even with a disciplined team.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026