Dockerfile Rules
These are the enforceable rules for writing production Dockerfiles - the ones you can lint with hadolint, verify in CI, and gate a merge on.
Search across all documentation pages
These are the enforceable rules for writing production Dockerfiles - the ones you can lint with hadolint, verify in CI, and gate a merge on.
The through-line is simple: small, reproducible, non-root images with no secrets baked in. Every rule below maps to a real failure mode or CVE class.
Wire the cheap checks into hadolint and a CI scan with Trivy or Grype. Review pull requests against the grouped items so no rule is silently skipped.
Each item is phrased as a gate. If a Dockerfile cannot pass it, it should not merge.
FROM node:22.14-slim@sha256:...; a floating tag can change under you and break reproducibility.-slim, alpine where glibc is not required, or gcr.io/distroless/* to shrink the attack surface.# build stage - toolchain lives here and never ships
FROM golang:1.24-bookworm AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app
# runtime stage - minimal, no compiler
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]COPY --from=build /out/app /app, not the whole build tree.RUN steps and clean up - remove apt caches in the same layer that created them..dockerignore - keep .git, node_modules, secrets, and test fixtures out of the build context.USER so Kubernetes runAsNonRoot can verify it.RUN useradd --uid 10001 --no-create-home --shell /usr/sbin/nologin appuser
USER 10001USER 10001 lets the kubelet enforce non-root without resolving a username.WORKDIR - avoid writing to / and make relative paths predictable.COPY or ENV a secret - layers are cached and distributable; a leaked layer leaks the secret forever.RUN --mount=type=secret exposes a credential only for that step, never in a layer.# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --omit=dev--mount=type=cache speeds builds without polluting the image.ENTRYPOINT in exec form - ENTRYPOINT ["/app"] avoids a shell wrapper and forwards signals correctly for clean shutdown.org.opencontainers.image.source and revision for provenance and traceability.hadolint in CI and fail on its findings.A note on the runtime boundary: Docker with BuildKit is your build and local-dev tool. It does not run your pods in the cluster - containerd executes them via the CRI on nodes, so these Dockerfile rules are about producing a good artifact, not about cluster runtime.
Why pin by digest when I already use a version tag?
A version tag like 22.14-slim is still mutable; the maintainer can repush it. The digest is content-addressed and cannot change.
Distroless has no shell - how do I debug?
Use an ephemeral debug container (kubectl debug) or a :debug distroless variant locally, keeping the production image shell-free.
Does a non-root USER in the Dockerfile satisfy Kubernetes?
It is necessary but pair it with securityContext.runAsNonRoot: true so the platform enforces it regardless of the image.
How do I pass build-time credentials for a private registry?
Use RUN --mount=type=secret with BuildKit, never ARG or ENV, because build args are visible in image history.
Alpine or distroless? Distroless for compiled languages and the smallest attack surface; slim or Alpine when you need a shell or glibc, accepting the larger surface.
Where should I bump base images? On a schedule and on CVE alerts, via an automated dependency bot, so digest pinning does not mean shipping stale bases.
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 16, 2026