78 lines | 3.6 KB

Dockerfile best practices — Claude Code rules

You are authoring or editing Dockerfiles. Follow Docker's official best practices below.

Images & base

  • Start from a current official (or Verified Publisher) image, and pick the minimal variant that fits — smaller base = faster pulls and smaller attack surface. Alpine is a common small choice (~6 MB).
  • Pin the version, and ideally the digest, for reproducible builds: FROM alpine:3.21@sha256:a8560b…. A bare alpine or alpine:latest drifts.
  • Don't install packages you don't need. Fewer deps = smaller, faster, safer.
  • Rebuild regularly with --pull (or --pull --no-cache) to pick up base-image and dependency security patches — images are immutable snapshots.

Multi-stage builds

  • Use multi-stage builds to separate the build environment from the final runtime image. Copy only the artifacts you need into the last stage.
  • Name stages (FROM node:22 AS build) and COPY --from=build the output, so compilers, dev deps, and source never ship in the runtime image.
  • Factor common setup into reusable stages (DRY) to avoid rebuilding it.

Caching & layer hygiene

  • Each instruction is a layer. When a layer changes, every layer after it is rebuilt. Order instructions least-changing → most-changing.
  • Install dependencies (which change rarely) before copying source (which changes constantly): copy the manifest, install, then copy the rest.
  • Combine related commands into a single RUN to reduce layers; clean up within the same RUN so the cleanup actually shrinks the layer.
  • Sort multi-line args alphanumerically so they're easy to scan and diff.

RUN / packages

  • For apt: RUN apt-get update && apt-get install -y --no-install-recommends pkg in one RUN, then rm -rf /var/lib/apt/lists/*. Splitting update from install causes stale-cache bugs.
  • Pin package versions (pkg=1.3.*) when you need determinism.
  • Prepend set -o pipefail && before piped commands so a failure mid-pipe fails the build.

COPY / ADD

  • Prefer COPY for files from the build context or another stage — it's explicit and predictable.
  • Reserve ADD for what only ADD does: remote URLs with checksum validation, or auto-extracting local tarballs.
  • Copy specific paths, not the whole context, to keep cache tight.

Runtime form (CMD / ENTRYPOINT / etc.)

  • Use exec form: CMD ["node", "server.js"] / ENTRYPOINT ["app"] — exec form receives Unix signals correctly. Shell form does not.
  • ENTRYPOINT for the fixed command, CMD for default args. In entrypoint scripts, exec "$@" so the app becomes PID 1.
  • Use absolute paths in WORKDIR; don't RUN cd … && ….
  • EXPOSE documents the listening port; it doesn't publish it.

Security

  • Run as non-root. Create a user explicitly and USER to it if the service doesn't need privileges. Avoid sudo; use gosu if you must drop privileges.
  • Never bake secrets. ENV, ARG, and COPY persist in image layers and history. Use RUN --mount=type=secret,id=… so the secret exists only during that build step and is never written to a layer.
  • Add a .dockerignore (like .gitignore) to keep .git, secrets, node_modules, and build cruft out of the context.

Don't

  • Don't use :latest or unpinned bases.
  • Don't run the app as root by default.
  • Don't pass secrets via ARG / ENV / COPY.
  • Don't run multiple concerns in one container — one concern per container.
  • Don't apt-get update in a separate layer from apt-get install.
  • Don't use shell-form CMD/ENTRYPOINT for long-running processes.