Multi-stage builds let you use one image for building/compiling and a separate, much smaller image for actually running your application — dramatically reducing final image size by excluding build-time-only dependencies.
The Problem Multi-Stage Builds Solve
Compiling/building an application often requires tools (compilers, build dependencies) that aren't needed at runtime — without multi-stage builds, these unnecessary tools remain in your final image, increasing size and attack surface for no runtime benefit.
Single-Stage Build (The Problem)
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
The final image includes the entire Go compiler toolchain, even though the compiled binary itself doesn't need it to run — unnecessarily large.
Multi-Stage Build (The Solution)
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
The first stage (builder) compiles the application using the full Go toolchain; the second stage starts fresh from a minimal Alpine base and copies only the compiled binary — the final image excludes the entire Go compiler and build tools.
Real-World Size Impact
A Go application built this way might result in a final image of just a few MB, compared to potentially hundreds of MB or more for a naive single-stage build including the full compiler toolchain — a substantial, meaningful difference.
Multi-Stage Builds for Node.js/JavaScript Applications
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
CMD ["node", "dist/server.js"]
Using Multiple Named Stages
FROM node:20 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20 AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]
More than two stages can further optimize caching — separating dependency installation from the build step means dependency changes don't invalidate the build stage's cache unnecessarily.
Benefits Beyond Just Size
- Smaller attack surface — fewer packages/tools in the final image means fewer potential vulnerabilities
- Faster deployment — smaller images transfer and start faster
- Cleaner separation between build-time and runtime dependencies
Verifying Image Size Reduction
docker images myapp
Compare sizes before and after implementing multi-stage builds to confirm the actual improvement for your specific application.
Common Errors
Runtime error about a missing dependency — verify you've copied everything the runtime actually needs from the build stage (compiled output, but also any runtime dependencies like node_modules for non-compiled languages).
Continue Reading
- How to Build Custom Docker Images with Dockerfile
- Understanding Docker Image Layers and Caching
- Docker Security Best Practices for Production Servers
Browse more articles in Docker & Containers.