Docker multistage build uses multiple FROM statements in one Dockerfile to separate the build environment from the runtime environment. The final image only contains the runtime dependencies - 10x smaller typically per the Docker docs. The team that uses multistage builds has small, secure images without manual cleanup steps.
Table of contents
- Single-stage vs multistage
- The COPY —from syntax
- Multiple build stages
- Naming stages
- Build specific stage
- Build cache optimization
- Common runtime base images
- FAQ
Single-stage vs multistage
Single-stage (everything in one image):
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o /app/server
CMD ["/app/server"]
Result: 800MB image (includes Go toolchain).
Multistage:
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o /server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /server /server
CMD ["/server"]
Result: 20MB image (only the compiled binary).
The team that uses multistage has tiny production images.
The COPY —from syntax
Copy artifacts from a previous stage:
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
The team that uses COPY --from=builder has the build output copied to the runtime stage.
Multiple build stages
FROM node:20 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20 AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
The team that separates deps, build, and runtime stages has caching at each layer.
Naming stages
Use AS name to name stages:
FROM golang:1.21 AS builder
# ...
FROM alpine:3.19 AS runtime
COPY --from=builder /app/server /server
The team that uses named stages has readable Dockerfiles.
Build specific stage
docker build --target builder -t myapp:builder .
Build only up to the builder stage. Useful for debugging the build without producing the full image.
Build cache optimization
Each stage is cached independently:
FROM node:20 AS deps
COPY package*.json ./
RUN npm ci # cached unless package.json changes
FROM node:20 AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
The team that orders stages for cache hits has fast incremental builds.
Common runtime base images
gcr.io/distroless/static-debian12: ~2MB, no shell, no package manager.alpine: ~5MB, includes shell + apk.debian-slim: ~80MB, full Debian base.ubuntu: ~100MB, full Ubuntu.
The team that picks distroless has smallest images + better security (fewer attack vectors).
FAQ
What’s the size difference between single-stage and multistage?
Typical: 10x smaller. Go app: 800MB -> 20MB. Node app: 1GB -> 50MB. The team that uses multistage has drastically smaller images.
Can I use multistage with BuildKit?
Yes - BuildKit (default in Docker 23+) has enhanced multistage support including parallel stage execution.
Do all stages get cached?
Yes - each stage is cached independently based on its inputs (FROM, COPY, RUN). The team that has well-ordered stages has fast rebuilds.
Can I copy from a previous image, not just stage?
Yes - COPY --from=otherimage:tag /path /dest. The team that copies artifacts from published images has flexible builds.
Should I use multistage for every Dockerfile?
Yes - multistage has no downside for production. For dev/test images that need the full toolchain, single-stage is fine.
If you are sizing the infrastructure for the kind of project this post covers, the RunxBuild hosting calculator is the right place to model the line items. The compute, the memory, the storage, the bandwidth, the database - each one is a separate number, and the team’s mental model for the platform is the sum of those numbers. The RunxBuild dashboard is where the team sees the actual usage in one place.
Useful related references: