Skip to main content

Docker Multi-Stage Builds: Optimization Patterns & Size Reduction

Multi-stage builds are the most effective way to reduce Docker image size and attack surface. By separating build-time dependencies from runtime requirements, images can shrink 5-20x while improving security and deploy speed.

✓ Node.js, Go, Python examples ✓ Build cache optimization ✓ COPY --link & --target ✓ Size comparison tables
Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$9/mo after trial · Cancel anytime

Want the full toolkit?

Get DevPlaybook Pro

Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →

Why Multi-Stage Builds Matter

A single-stage Dockerfile for a Node.js app installs all dev dependencies, build tools, and generates artifacts in one layer. The final image contains everything — webpack, TypeScript compiler, test frameworks — that your production server never needs.

Approach Node.js App Go Binary Python Service
Single-stage (full image)~1.4 GB~1.2 GB~3.5 GB
Multi-stage (alpine)~120 MB~15 MB~400 MB
Multi-stage (distroless)~100 MB~5 MB~200 MB

Pattern 1: Node.js Builder + Nginx Runner

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app

# Cache dependency install separately from source code
COPY package.json package-lock.json ./
RUN npm ci --prefer-offline

# Now copy source and build
COPY . .
RUN npm run build

# Stage 2: Production (only nginx + built assets)
FROM nginx:1.27-alpine AS production
# Copy built artifacts from builder stage — nothing else
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# Result: ~25MB vs ~1.4GB single-stage

Pattern 2: Go Static Binary

# Stage 1: Build with full Go toolchain
FROM golang:1.22-alpine AS builder
WORKDIR /build

# Cache module download
COPY go.mod go.sum ./
RUN go mod download

COPY . .
# CGO_ENABLED=0 produces a fully static binary (no libc dependency)
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o server .

# Stage 2: Scratch — literally nothing (0 bytes base)
FROM scratch AS production
COPY --from=builder /build/server /server

# Copy CA certs for HTTPS requests
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080
ENTRYPOINT ["/server"]

# Result: ~7MB vs ~1.2GB single-stage

Pattern 3: Python with BuildKit Cache Mounts

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app

# BuildKit cache mount: pip cache persists between builds
# (not stored in image layers)
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --prefix=/install -r requirements.txt

# Stage 2: Minimal runtime
FROM python:3.12-slim AS production
WORKDIR /app

# Copy only installed packages
COPY --from=builder /install /usr/local
COPY src/ ./src/

# Non-root user for security
RUN useradd -m appuser
USER appuser

CMD ["python", "-m", "src.main"]

Build Cache Optimization Order

Layer order determines cache effectiveness. Put the slowest, least-changing operations first:

Bad (cache invalidated on any source change)

COPY . .              # All source files
RUN npm ci            # Reinstalls every time!

Good (dependencies cached separately)

COPY package*.json .  # Only manifest
RUN npm ci            # Cached until manifest changes
COPY . .              # Source changes don't bust dep cache

Using --target for Development Builds

# Build specific stages
docker build --target builder -t myapp:dev .    # Dev image with all tools
docker build --target production -t myapp:prod . # Lean production image

# Multi-stage for CI: run tests in builder, build prod separately
docker build --target test -t myapp:test .
docker run --rm myapp:test npm test

# Only build prod if tests pass
docker build --target production -t myapp:latest .