How Multi-Stage Docker Builds Optimize Image Size
How Docker layer caching, multi-stage builds, and distroless base images reduce production container image sizes from gigabytes to megabytes.
Introduction
A bloated Docker image is a security liability and a deployment bottleneck. An image containing build tools, compilers, test frameworks, and node_modules can easily exceed 1 GB β causing slow CI pipelines (longer pull times), larger attack surface (more packages = more CVEs), and wasted registry storage. Multi-stage builds solve this by using one stage to compile/build the application and a separate minimal stage for the runtime image, copying only the compiled artifact.
Step-by-Step: Building Lean Images
flowchart TD
A["1. NaΓ―ve Single-Stage Build (The Problem)"]
B["2. Multi-Stage Build"]
C["3. Understanding Layer Caching"]
D["4. Distroless vs. Alpine vs. Scratch"]
E["5. Node.js Multi-Stage Example"]
A --> B
B --> C
C --> D
D --> E
Step 1: NaΓ―ve Single-Stage Build (The Problem)
# β Bloated: includes Go toolchain, source, and intermediate files
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go mod download && go build -o api-server ./cmd/api
EXPOSE 8080
CMD ["/api-server"]
# Result: ~850 MB image (Go SDK + source + binary)
Step 2: Multi-Stage Build
# β
Stage 1: Build (heavyweight)
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Copy dependency manifests first (cache busting optimization)
COPY go.mod go.sum ./
RUN go mod download
# Copy source and compile
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o api-server ./cmd/api
# β
Stage 2: Runtime (lightweight)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/api-server /api-server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/api-server"]
# Result: ~8 MB image
Step 3: Understanding Layer Caching
Docker caches each RUN, COPY, and ADD instruction as a layer. Layers are invalidated when the instruction or any prior layer changes:
# β Bad: copies entire source first β any file change invalidates go mod download
COPY . .
RUN go mod download
# β
Good: copy dependency files first, source second
# go.mod/go.sum rarely change β RUN go mod download stays cached
COPY go.mod go.sum ./
RUN go mod download
COPY . . # Cache miss here only invalidates build step
RUN go build ...
Step 4: Distroless vs. Alpine vs. Scratch
| Base Image | Size | Shell | Package Manager | Use Case |
|---|---|---|---|---|
ubuntu:22.04 | 77 MB | bash | apt | Development, debugging |
alpine:3.19 | 7 MB | sh | apk | Small with shell access |
gcr.io/distroless/static | 2 MB | None | None | Go/Rust static binaries |
scratch | 0 MB | None | None | Fully static binaries only |
Distroless images contain only the app and its runtime dependencies β no shell, no package manager, no coreutils. This eliminates entire classes of shell injection vulnerabilities.
Step 5: Node.js Multi-Stage Example
# Stage 1: Install all dependencies and build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --include=dev
COPY . .
RUN npm run build # TypeScript compile, bundle
# Stage 2: Production runtime
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev # Only production deps
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Result: ~120 MB vs ~450 MB with dev deps
Key Takeaways
- Multi-stage builds use a builder stage with full toolchain and a runtime stage with only the compiled artifact β reducing images from GB to MB.
- Layer cache ordering is critical: copy dependency manifests before source code so dependency installs are cached across source changes.
- Distroless base images have zero shell or package manager β eliminating attack surface for runtime exploits.
- The
-ldflags="-s -w"Go build flag strips debug symbols and DWARF info, reducing binary size by ~30%. - Use
docker history <image>anddivetool to inspect layer sizes and identify bloat.
Historical figures, architectures, and capabilities are for informational purposes only. Not technical, professional, legal, or financial advice. Sources: Research papers, developer documentation.