Skip to main content
Cloud & AI Hub
Browse
Glossary AI Directory Playgrounds Models Prompts Explainers Strategy Matrix Benchmark Decoder
beginner 6 mins Read

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 ImageSizeShellPackage ManagerUse Case
ubuntu:22.0477 MBbashaptDevelopment, debugging
alpine:3.197 MBshapkSmall with shell access
gcr.io/distroless/static2 MBNoneNoneGo/Rust static binaries
scratch0 MBNoneNoneFully 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> and dive tool 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.