Multi-Stage Build — Building Small, Secure Production Images
What Is a Multi-Stage Build in Simple Terms?
A multi-stage build solves one of the most common Docker problems: build tools are heavy, but you only need them during the build. Your Go compiler is 500MB. Your final binary is 10MB. A single-stage Dockerfile keeps both in the image forever. A multi-stage build uses the compiler in one stage, copies only the binary to the final stage, and throws everything else away.
The result is a production image that is 5-10x smaller and has dramatically fewer CVEs because it contains no build tools, no compilers, no test frameworks — just what the running application needs.
+------------------------------------------+| Stage 1: builder || FROM node:20 AS builder || Install ALL deps (dev + prod) || Copy source code || Run build (TypeScript -> JavaScript) || Result: compiled output in /app/dist |+------------------------------------------+ | COPY --from=builder /app/dist | (only the compiled output) v+------------------------------------------+| Stage 2: production (final image) || FROM node:20-alpine || Install ONLY production deps || Copy compiled output from builder || No source code, no dev tools, no tsc || Result: 180MB instead of 1.8GB |+------------------------------------------+Complete Multi-Stage Dockerfile Examples
Node.js / TypeScript:
# syntax=docker/dockerfile:1 # Stage 1 — BuildFROM node:20-alpine AS builderWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci # all deps including devDependenciesCOPY . .RUN npm run build # TypeScript -> dist/ # Stage 2 — ProductionFROM node:20-alpine AS productionWORKDIR /appCOPY package.json package-lock.json ./RUN npm ci --omit=dev # production deps onlyCOPY --from=builder /app/dist ./dist RUN addgroup -S app && adduser -S app -G appUSER app EXPOSE 8080CMD ["node", "dist/server.js"] # Before multi-stage: ~1.8GB# After multi-stage: ~178MBGo — the most dramatic size reduction:
# Stage 1 — BuildFROM golang:1.21-alpine AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -o payment-api ./cmd/server # Stage 2 — Final (scratch = empty image)FROM scratchCOPY --from=builder /app/payment-api /payment-apiCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/EXPOSE 8080CMD ["/payment-api"] # Before multi-stage: ~400MB (Go toolchain)# After multi-stage: ~8MB (just the binary)Python:
# Stage 1 — Build dependenciesFROM python:3.11-slim AS builderWORKDIR /appCOPY requirements.txt .RUN pip install --user --no-cache-dir -r requirements.txt # Stage 2 — ProductionFROM python:3.11-slim AS productionWORKDIR /appCOPY --from=builder /root/.local /root/.localCOPY . .ENV PATH=/root/.local/bin:$PATHCMD ["python", "app.py"]Naming Stages and Copying Between Them
# Name a stage with ASFROM node:20-alpine AS depsFROM node:20-alpine AS builderFROM node:20-alpine AS production # Copy from a named stageCOPY --from=deps /app/node_modules ./node_modulesCOPY --from=builder /app/dist ./dist # Copy from a specific stage by index (0-based)COPY --from=0 /app/output ./output # Copy from an external imageCOPY --from=nginx:1.25 /etc/nginx/nginx.conf /etc/nginx/nginx.confBuilding Specific Stages
# Build only the builder stage (useful for debugging)docker build --target builder -t payment-api:builder . # Then get a shell inside itdocker run --rm -it payment-api:builder sh# Inspect what was built, check for errors # Build the final production stage (default)docker build -t payment-api:latest . # Compare sizesdocker images | grep payment-api# payment-api builder 1.82GB# payment-api latest 178MBTroubleshooting Reference
| Problem | Cause | Fix |
|---|---|---|
| Files not found in final stage | Wrong path in COPY --from | Check exact output path in builder stage with docker build --target builder |
| Native module errors in Alpine final stage | musl vs glibc mismatch | Use node:20-slim instead of node:20-alpine as final base |
| Final image still large | Copying too much from builder | Only copy what is needed: COPY --from=builder /app/dist ./dist not COPY --from=builder /app . |
| Build fails on multi-platform | Architecture mismatch between stages | Use --platform=$BUILDPLATFORM on builder stage |
PLACEMENT PRO TIP**Tip:** Use `docker build --target builder -t debug-build .` to build and inspect the builder stage when your build is failing. You get a full shell inside the build environment with all dev tools available, making it easy to diagnose compilation or dependency errors.
REMEMBER THIS**Remember:** In a multi-stage build, only the last `FROM` stage becomes the final image. All previous stages are discarded — they exist only as intermediate build environments. The intermediate stages are cached but not pushed to the registry.
COMMON MISTAKE / WARNING**Common Mistake:** Copying the entire `/app` directory from the builder stage instead of only the build output. `COPY --from=builder /app .` copies source code, node_modules, temp files, and everything else. Always copy only the specific output directory: `COPY --from=builder /app/dist ./dist`.