Multi-stage builds are the single highest-impact optimisation available in Docker. They solve the fundamental tension between “I need build tools to compile my application” and “I don’t want build tools in my production image” — without requiring complex external scripts or two separate Dockerfiles.
The idea is simple: use one or more disposable builder stages to produce an artifact, then copy only that artifact into a minimal runtime stage. The builder stages are discarded at the end of the build and are never pushed to a registry.
Consider a Python application. To install a package with C extensions (like psycopg2), you need gcc, libpq-dev, and other build headers. Once installed, the compiled .so file is all that matters — gcc serves no purpose in the running container.
Without multi-stage builds, you have two bad choices:
Multi-stage builds give you a third option: build inside Docker, discard the build environment.
A multi-stage Dockerfile uses multiple FROM instructions. Each FROM starts a new stage. Stages can be named with AS <name>:
# Stage 1: builder
FROM python:3.12-slim-bookworm AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Stage 2: runtime
FROM python:3.12-slim-bookworm
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["python", "main.py"]
The key instruction is COPY --from=builder, which copies files from a named stage rather than from the build context. The builder stage — including any packages installed in it, any cached files, any intermediate build artifacts — is not present in the runtime image.
Full example: code/dockerfiles/python_multistage.Dockerfile
A cleaner Python pattern isolates dependencies into a virtualenv, then copies the entire virtualenv to the runtime stage:
FROM python:3.12-slim-bookworm AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim-bookworm
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . /app
WORKDIR /app
USER nobody
CMD ["python", "main.py"]
This pattern is self-contained (the venv includes all metadata), easy to audit, and works reliably across all Python package types.
Go’s static compilation makes multi-stage builds particularly powerful. The builder stage needs the full Go toolchain; the runtime stage needs only the compiled binary:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOARCH=amd64 go build -ldflags="-s -w" -o server .
FROM scratch
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
The resulting image is FROM scratch plus a single binary. Typical size: 12–25 MB.
The flags -ldflags="-s -w" strip debug symbols and DWARF information from the binary, typically reducing its size by 20–30%.
Full example: code/dockerfiles/go_scratch.Dockerfile
You can build only up to a specific stage using --target:
docker build --target builder -t myapp:builder .
This is useful for:
test stage between builder and runtime; run tests in CI by targeting the test stage.dev stage that includes dev dependencies and hot-reload tools; production uses the runtime stage.There is no requirement that stages use the same base image. A common pattern for Node.js is:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/index.js"]
The builder stage installs all dependencies (including dev) and runs the build. The runtime stage installs only production dependencies and copies the compiled output.
Full example: code/dockerfiles/node_multistage.Dockerfile
For complex builds, you can chain multiple named stages:
FROM python:3.12-slim AS deps
# install production dependencies
FROM deps AS test
# install test dependencies on top, run tests
FROM python:3.12-slim AS runtime
# copy only from deps, not test
COPY --from=deps /opt/venv /opt/venv
This structure allows CI to build and run the test stage without those test dependencies ever reaching the runtime image.
FROM instruction starts a new stage; use AS <name> to make stages addressable.COPY --from=<stage> copies files between stages without inheriting the source stage’s filesystem.CGO_ENABLED=0 and deploy to FROM scratch.--target to build specific stages in CI for testing or debugging.| ← Chapter 2: Choosing the Right Base Image | Table of Contents | Chapter 4: Dockerfile Instruction Optimization → |