Dockerfile development best practices. Use when creating, modifying, or reviewing Dockerfiles, .dockerignore files, or container image build configurations.
FROM node:24.13-alpine3.21latestFROM node:24.13-alpine3.21@sha256:abc123....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"]
FROM --platform=linux/amd64 image:tag.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/
RUN, COPY, ADD creates a layer. ENV, LABEL, EXPOSE, WORKDIR are metadata-only.RUN — separate layers retain deleted files.COPY --link for independent layers that build in parallel.COPY --parents (BuildKit) to preserve directory structure: COPY --parents src/app/*.py ./.Local layer cache doesn't persist between CI runs. Use external cache backends:
- uses: docker/build-push-action@v6
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max caches all layers (including intermediate stages), not just the final image.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 .
docker buildx build \
--cache-from type=local,src=/tmp/.buildx-cache \
--cache-to type=local,dest=/tmp/.buildx-cache-new,mode=max .
Rotate cache directories to prevent unbounded growth.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.
COPY --from=builder /path /path to copy between stages.docker build --target test ..ADD only for local tar auto-extraction. For URLs, use curl/wget in RUN.CMD and ENTRYPOINT:
CMD ["node", "server.js"] # correct: receives signals
# CMD node server.js # wrong: wraps in /bin/sh -c, breaks signals
docker run):
ENTRYPOINT ["python", "-m", "myapp"]
CMD ["--port=8080"]
docker history). ENV persists in the image. ARG before FROM is scoped to the FROM line only.RUN mkdir -p /app && cd /app. Creates the dir and sets it for all subsequent instructions.USER after all RUN instructions that need root. Platform-specific user creation: see patterns/security-hardening.md — Non-root users.ARG SECRET, no COPY .env, no ENV API_KEY=.... Use BuildKit --mount=type=secret (see patterns/security-hardening.md — BuildKit secrets).cosign sign --key cosign.key ghcr.io/org/myapp@sha256:abc.... Verify in CI or Kubernetes admission controller (Kyverno, Connaisseur) before deploy.cosign verify --key cosign.pub ghcr.io/org/myapp@sha256:abc.... Reject unsigned images in the deploy pipeline.docker buildx build \
--sbom=true \
--provenance=mode=max \
-t ghcr.io/org/myapp:1.0.0 \
--push .
Attestations are stored as OCI artifacts alongside the image. Verify with cosign verify-attestation.COPY --chown=appuser:appgroup to set ownership without extra layers.RUN (see Layer Ordering example above). Or use cache mounts: see patterns/optimization-patterns.md — Cache mounts.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.
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.
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.
CMD ["binary"] runs as PID 1 and receives signals directly.CMD binary runs under /bin/sh -c — SIGTERM goes to shell, not the app.exec to replace the shell, or use tini:
RUN apk add --no-cache tini
ENTRYPOINT ["tini", "--"]
CMD ["node", "server.js"]
FROM node:latest or untagged — non-reproducible builds. Pin version + digest.ADD for copying files — use COPY. ADD only for local tar extraction. (instruction-reference.md — COPY vs ADD)CMD node server.js — wraps in /bin/sh -c, breaks signal handling. Use exec form CMD ["node", "server.js"].ARG SECRET=... or COPY .env — secrets baked into image layers, recoverable with docker history. Use --mount=type=secret. (patterns/security-hardening.md — BuildKit secrets)RUN apt-get update and RUN apt-get install — cache cleaned in a later layer is still stored in the earlier layer. Combine in one RUN.USER — container runs as root. Always add a non-root user before CMD.VOLUME /data then RUN modifying /data — changes silently discarded. (instruction-reference.md — VOLUME).dockerignore — sends .git, node_modules, .env into build context.- [ ] 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)
hadolint Dockerfile — fix all warnings (DL=Dockerfile rules, SC=ShellCheck rules)docker build --no-cache -t test . — fix build errorstrivy image test or grype test — fix CVEs in base image or depsdocker run --rm test — verify the container starts and worksMulti-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
org.opencontainers.image.* keys and formatting rules