Dockerfile Development Best Practices
Base Images
- Always use the current stable release when generating Dockerfiles. Don't copy version numbers from these templates — check the latest: go.dev/dl, hub.docker.com/_/node, hub.docker.com/_/python, hub.docker.com/_/rust.
- Pin to specific version:
FROM node:24.13-alpine3.21. Neverlatestor untagged. - Pin by digest for reproducibility:
FROM node:24.13-alpine3.21@sha256:abc123.... - Prefer minimal bases: distroless > alpine/slim > full.
distroless: no shell, no package manager — smallest attack surface, production only.alpine: ~5MB, has shell and apk — good default for Go/Rust static binaries. Can cause issues with Python/Node/Java native modules due to musl libc.slim: ~70MB Debian with minimal packages — preferred for Python, Node.js, Java (glibc compatibility).scratch: empty image, zero overhead — for fully static Go or Rust binaries with no libc dependency:FROM scratch COPY --from=build /app/server /server EXPOSE 8080 CMD ["/server"]
- Cross-architecture:
FROM --platform=linux/amd64 image:tag.
Layer Ordering & Caching
Order instructions from least-changed to most-changed:
# 1. System deps (rarely change)
RUN apt-get update && apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
# 2. App dependency files (change occasionally)
COPY package.json package-lock.json ./
RUN npm ci
# 3. Source code (changes frequently)
COPY src/ src/
- Each
RUN,COPY,ADDcreates a layer.ENV,LABEL,EXPOSE,WORKDIRare metadata-only. - Combine install + cleanup in the same
RUN— separate layers retain deleted files. - Use
COPY --linkfor independent layers that build in parallel. - Use
COPY --parents(BuildKit) to preserve directory structure:COPY --parents src/app/*.py ./.
CI/CD Build Caching
Local layer cache doesn't persist between CI runs. Use external cache backends:
- GitHub Actions:
- uses: docker/build-push-action@v6 with: context: . cache-from: type=gha cache-to: type=gha,mode=maxmode=maxcaches all layers (including intermediate stages), not just the final image. - Registry cache: push cache layers to a registry, pull on next build.
docker buildx build \ --cache-from type=registry,ref=ghcr.io/org/myapp:cache \ --cache-to type=registry,ref=ghcr.io/org/myapp:cache,mode=max \ -t ghcr.io/org/myapp:latest . - Local cache (self-hosted runners):
Rotate cache directories to prevent unbounded growth.docker buildx build \ --cache-from type=local,src=/tmp/.buildx-cache \ --cache-to type=local,dest=/tmp/.buildx-cache-new,mode=max .
Multi-stage Builds
Always multi-stage. Name every stage — never reference by index.
# syntax=docker/dockerfile:1
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM deps AS build
COPY tsconfig.json ./
COPY src/ src/
RUN npm run build
FROM node:24-alpine AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Always start with # syntax=docker/dockerfile:1 — enables cache mounts, secret mounts, SSH mounts, heredocs, and COPY --link.
- Build stage: compilers, dev deps. Runtime stage: only artifacts.
COPY --from=builder /path /pathto copy between stages.- Separate test target for CI:
docker build --target test .. - Full language-specific templates: see patterns/multi-stage-templates.md.
Instructions
- COPY over ADD always.
ADDonly for local tar auto-extraction. For URLs, usecurl/wgetinRUN. - Exec form for
CMDandENTRYPOINT:CMD ["node", "server.js"] # correct: receives signals # CMD node server.js # wrong: wraps in /bin/sh -c, breaks signals - ENTRYPOINT + CMD combo: ENTRYPOINT for the binary, CMD for default args (overridable at
docker run):ENTRYPOINT ["python", "-m", "myapp"] CMD ["--port=8080"] - ARG vs ENV: ARG is build-time only (and visible in
docker history). ENV persists in the image. ARG before FROM is scoped to the FROM line only. - WORKDIR /app over
RUN mkdir -p /app && cd /app. Creates the dir and sets it for all subsequent instructions. - EXPOSE is documentation only — does not publish ports.
Security
- Run as non-root. Add
USERafter allRUNinstructions that need root. Platform-specific user creation: see patterns/security-hardening.md — Non-root users. - Never bake secrets into images. No
ARG SECRET, noCOPY .env, noENV API_KEY=.... Use BuildKit--mount=type=secret(see patterns/security-hardening.md — BuildKit secrets). - Sign images:
cosign sign --key cosign.key ghcr.io/org/myapp@sha256:abc.... Verify in CI or Kubernetes admission controller (Kyverno, Connaisseur) before deploy. - Verify before pull:
cosign verify --key cosign.pub ghcr.io/org/myapp@sha256:abc.... Reject unsigned images in the deploy pipeline. - SBOM + provenance attestations: generate and attach via BuildKit — required for SLSA Level 2+ and supply chain compliance:
Attestations are stored as OCI artifacts alongside the image. Verify withdocker buildx build \ --sbom=true \ --provenance=mode=max \ -t ghcr.io/org/myapp:1.0.0 \ --push .cosign verify-attestation. COPY --chown=appuser:appgroupto set ownership without extra layers.- Clean package caches in the same
RUN(see Layer Ordering example above). Or use cache mounts: see patterns/optimization-patterns.md — Cache mounts.
.dockerignore
Always create one. Without it, the entire directory (.git, node_modules, local env) goes into the build context — on large repos this can send GBs to the daemon and dramatically slow builds.
Universal + language-specific templates: see patterns/optimization-patterns.md — .dockerignore patterns.
Labels & Metadata
Use OCI image-spec annotation keys (org.opencontainers.image.*). All values must be strings. Custom keys use reverse-domain notation (com.example.mykey).
At minimum, include title, version, revision, created, source, and licenses. Full label template and formatting rules: see instruction-reference.md — LABEL.
Health Checks
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["curl", "-f", "http://localhost:8080/healthz"]
For distroless (no curl): compile a static healthcheck binary or use the runtime's built-in health endpoint.
Signal Handling
- Exec form
CMD ["binary"]runs as PID 1 and receives signals directly. - Shell form
CMD binaryruns under/bin/sh -c— SIGTERM goes to shell, not the app. - For scripts as entrypoints:
execto replace the shell, or usetini:RUN apk add --no-cache tini ENTRYPOINT ["tini", "--"] CMD ["node", "server.js"]
Common mistakes
FROM node:latestor untagged — non-reproducible builds. Pin version + digest.ADDfor copying files — useCOPY.ADDonly for local tar extraction. (instruction-reference.md — COPY vs ADD)- Shell form
CMD node server.js— wraps in/bin/sh -c, breaks signal handling. Use exec formCMD ["node", "server.js"]. ARG SECRET=...orCOPY .env— secrets baked into image layers, recoverable withdocker history. Use--mount=type=secret. (patterns/security-hardening.md — BuildKit secrets)- Separate
RUN apt-get updateandRUN apt-get install— cache cleaned in a later layer is still stored in the earlier layer. Combine in oneRUN. - Missing
USER— container runs as root. Always add a non-root user beforeCMD. VOLUME /datathenRUNmodifying/data— changes silently discarded. (instruction-reference.md — VOLUME)- Missing
.dockerignore— sends.git,node_modules,.envinto build context.
New Dockerfile workflow
- [ ] Choose minimal base image with pinned version
- [ ] Create .dockerignore
- [ ] Design multi-stage build (deps -> build -> runtime)
- [ ] Order layers for cache efficiency
- [ ] Add non-root USER
- [ ] Add HEALTHCHECK
- [ ] Add OCI labels
- [ ] Run validation loop (below)
Validation loop
hadolint Dockerfile— fix all warnings (DL=Dockerfile rules, SC=ShellCheck rules)docker build --no-cache -t test .— fix build errorstrivy image testorgrype test— fix CVEs in base image or depsdocker run --rm test— verify the container starts and works- Repeat until hadolint is clean, scan is acceptable, container runs correctly
Deep-dive references
Multi-stage templates: See patterns/multi-stage-templates.md for Go, Node.js, Python, Java, Rust Security hardening: See patterns/security-hardening.md for distroless, secrets, scanning Optimization: See patterns/optimization-patterns.md for cache mounts, BuildKit, image size Instruction gotchas: See instruction-reference.md for per-instruction cheatsheet
Official references
- Dockerfile reference — all instructions, syntax, escape directives
- Docker Build best practices — official guidance on layers, caching, multi-stage
- OCI image spec: annotations — full list of
org.opencontainers.image.*keys and formatting rules - hadolint rules — DL/SC rule reference