Guide

A Dockerfile is a recipe. Most lines create a layer, and the builder caches each one. A step is reused when its instruction and inputs have not changed. Once one step misses the cache, every step after it rebuilds. The core skill is ordering steps from the least often changed to the most often changed.

Concept

A cache hit means the builder reuses a layer it already built. Changing a file that a step copies in is a cache miss for that step and for every step below it.

Walking through layer caching

This Dockerfile reinstalls every dependency on every code change.

dockerfile
FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "main.py"]

COPY . . copies the whole project, so any source edit misses the cache before pip install runs. Copy the dependency list first instead.

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]

Now a code-only edit rebuilds only the last small layer, in a second or two. The same pattern works for package.json with its lockfile, go.mod with go.sum, and Cargo.toml with Cargo.lock.

Multi-stage builds

Compilers and headers are needed at build time, not at runtime. A multi-stage build compiles in one stage and copies only the output into a clean final stage.

dockerfile
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/api ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/api /api
USER nonroot
ENTRYPOINT ["/api"]

The build stage is about 800 MB. The final image holds the static binary plus a few MB of certificates and timezone data, so about 15 to 25 MB in total. The build stage is thrown away.

Choosing a base image

BaseRough sizeNotes
python:3.12 (full Debian)~1 GBCompilers included. Fine for a build stage only
python:3.12-slim~120–150 MBDebian with minimal packages. The default for Python
python:3.12-alpine~50–60 MBmusl libc. Many PyPI wheels need glibc, so pip may compile from source
distroless/python3-debian12~50–60 MBNo shell and no package manager. Smaller attack surface
distroless/static, scratch~2 MB / 0For static binaries such as Go or Rust

Alpine is excellent for static binaries. For Python, Node with native modules, or the JVM, prefer -slim or distroless.

Run as non-root, and ignore what you don't need

By default the process runs as root (UID 0). Create an unprivileged user with a numeric ID, so Kubernetes can check runAsNonRoot without looking up names.

dockerfile
RUN useradd --uid 10001 --no-create-home appuser
USER 10001

Non-root processes cannot bind ports below 1024 by default, so listen on 8080. Also add a .dockerignore that excludes .git, node_modules, local virtualenvs and .env files. Without it, COPY . . sends secrets and junk into the build context.

Common mistakes

  • Copying all source before installing dependencies. Every commit reinstalls everything.
  • No .dockerignore. Secrets and local folders end up in the build context.
  • Using Alpine for a Python app with compiled dependencies. Pip may compile from source, which is slow and can make the image larger.
  • Running as root by default. A container escape then starts as root on the host.

Check yourself

  1. You edit one line of source, and pip install runs again from scratch. Why? Answer: COPY . . comes before pip install, so the source change invalidates the install step's cache.
  2. Why is a multi-stage image smaller? Answer: Only the built output is copied into a clean final stage. Compilers stay in the stage that is discarded.

Key takeaways

  • Order steps from least to most often changed, so the cache does the work.
  • Multi-stage builds keep compilers and headers out of the runtime image.
  • Start from a slim or distroless base, and pin it by digest in production.
  • Run as a numeric non-root user with an exec-form CMD.

Further learning

Docker's build best practices page covers caching, multi-stage builds and base images in more depth.

Docker: Building best practicesHints, tips and guidelines for writing clean, reliable DockerfilesDocker Documentation

The next page works through a real image that was reduced from 1.2 GB: shrinking an image.