DevOps Peach

Docker & Kubernetes · Building Docker Images with Dockerfile · Lesson 4 of 4

Multi-stage builds and the build cache

Split the build stage from the production stage so the final image only holds the artifact; order instructions to use the cache.

Practice interactively →

Why images must be small

A large production image is slow to pull and deploy, uses more storage and carries extra packages, which means more potential vulnerabilities (a larger attack surface). Compilers, dev node_modules and source code do not belong in the image that runs in production.

Multi-stage build

FROM node:20-alpine AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /src/dist /usr/share/nginx/html
flowchart LR
  S1["build stage<br/>node + npm + source"] -- "COPY --from=build /src/dist" --> S2["final stage<br/>nginx + dist"]
  S2 --> IMG["Image app:1.0"]

Each FROM opens a stage. The resulting image is only the last stage (or the stage chosen with --target); earlier stages are left behind. --target build is handy for running tests in the build stage.

Build cache

Docker walks through each instruction: if the instruction and its inputs (files being COPYed) are unchanged, it reuses the old layer (cache hit). When a step is a cache miss, that step and every step after it are rebuilt.

Command Meaning
docker build --target build -t app:build . Build only up to the build stage
docker build --no-cache -t app:1.0 . Rebuild every step without the cache
docker build -f Dockerfile.prod -t app:prod . Use a Dockerfile with a non-default name
docker image prune Remove dangling (untagged) images

BuildKit and buildx

Modern Docker builds with BuildKit by default. It runs independent stages in parallel, skips stages the target does not need, and supports cache and secret mounts. docker buildx is the CLI for advanced builders, for example ones running in a container or on a remote machine.

Cache mounts

A cache mount keeps a package manager's download cache between builds without putting it in the image:

RUN --mount=type=cache,target=/root/.npm npm ci
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt

When package.json changes, the step reruns but reuses already downloaded packages, which makes rebuilds much faster.

Cache in CI

CI runners often start empty, so the local cache is lost. BuildKit can export and import cache through a registry:

docker buildx build --cache-from type=registry,ref=registry.example.com/team/app:cache \
  --cache-to type=registry,ref=registry.example.com/team/app:cache,mode=max \
  -t registry.example.com/team/app:1.0 --push .

GitHub Actions also has a dedicated type=gha cache backend.

Building for several CPUs

docker buildx build --platform linux/amd64,linux/arm64 -t registry.example.com/team/app:1.0 --push .

This produces one tag with both variants, useful when developers use Apple Silicon laptops and servers run on x86 or Arm cloud VMs.

Checking what you build

A small final stage, non-root user, pinned base versions, and regular rebuilds to pick up patched base images keep production images healthy over time.

Practice interactively →