Containers and orchestration
Writing good Dockerfiles
Order Dockerfile steps so the cache works for you, build in stages so compilers stay out of the final image, and run as a non-root user.
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.
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.
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.
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
| Base | Rough size | Notes |
|---|---|---|
python:3.12 (full Debian) | ~1 GB | Compilers included. Fine for a build stage only |
python:3.12-slim | ~120–150 MB | Debian with minimal packages. The default for Python |
python:3.12-alpine | ~50–60 MB | musl libc. Many PyPI wheels need glibc, so pip may compile from source |
distroless/python3-debian12 | ~50–60 MB | No shell and no package manager. Smaller attack surface |
distroless/static, scratch | ~2 MB / 0 | For 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.
RUN useradd --uid 10001 --no-create-home appuser
USER 10001Non-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
- You edit one line of source, and
pip installruns again from scratch. Why? Answer:COPY . .comes beforepip install, so the source change invalidates the install step's cache. - 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 DocumentationThe next page works through a real image that was reduced from 1.2 GB: shrinking an image.
Related
Image layers and digests
An image is a stack of read-only layers named by their content hash. Learn why deleting a file does not shrink an image, and why production should pin digests, not tags.
Shrinking an image
A worked example that takes a 1.2 GB Python image under 150 MB. Measure each layer first, then fix the largest costs one at a time.
Containers from the inside
Namespaces, cgroups and layered images - what a container really is, how images are built and shipped, and how to write small, fast, secure Dockerfiles.