Compile a binary in one stage, copy only the binary into a tiny final image without compilers, headers, or source.
multi-stage is making a dish in a big kitchen, then packing only the meal into a lunchbox - the cutting boards and ovens stay home.
Stage 1 (builder): Stage 2 (runtime):
FROM golang:1.22 FROM gcr.io/distroless/static
RUN go build -o /app COPY --from=builder /app /app
CMD ["/app"]
image size: 1.2GB image size: 12MB
(compiler, source) (only binary, no shell)
Multi-stage builds let you do everything in stage builder then COPY --from=builder only the artix to a slim final stage. Smaller attack surface, faster pulls, less disk. Use a minimal runtime base (alpine, distroless, scratch). For interpreted languages (Python, Node), still extract wheels or node_modules only, removing build tools.
FROM python:3.12 AS builder
RUN pip wheel --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /wheels /wheels
RUN pip install --no-index --find-links=/wheels /wheels/*
COPY . /app
the second stage installs only run-time deps; build-only wheels aren’t shipped; final image ~150MB instead of ~400MB.
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/node_modules /app/node_modules
CMD ["node", "app.js"]
slim runtime with only the production build; devDeps are in the builder stage only.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /out
FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY --from=build /out /app
CMD ["dotnet", "app.dll"]
.NET pattern: SDK huge (~1GB), aspnet base small (~150MB); final image just has published output.
Forgetting --from=builder and accidentally copying from the wrong stage. Using scratch and forgetting to put a CA bundle in.
multi-stage for any compiled language; for interpreted, still extract wheels or node_modules; use named stages for clarity; tag the cache from each stage for fast CI.
AWS_SECRET_ACCESS_KEY baked into it.docker history.RUN AWS_SECRET=... && npm run build in the same stage as the final image.--no-cache and proper BuildKit secrets.RUN --mount=type=secret,id=aws_creds for any secret during build.ubuntu:22.04 directly without multi-stage; carried all distro packages.gcr.io/distroless/static; result typically under 10 CVEs.