After optimizing hundreds of Docker images for production, I’ve seen multi-stage builds reduce image sizes by 70-90%. This isn’t just about saving disk space—smaller images mean faster deployments, reduced attack surface, and lower cloud costs. This guide shows production-tested multi-stage build patterns.
1. The Docker Image Bloat Problem
Without multi-stage builds:
- Huge images: 2GB+ images for simple Python apps
- Build tools in production: gcc, make, git unnecessary at runtime
- Slow deployments: Pulling 2GB vs 200MB makes a difference
- Security risks: More packages = more vulnerabilities
2. Multi-Stage Build Basics
# Single-stage (BAD): 1.2GB image
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# Multi-stage (GOOD): 200MB image
# Stage 1: Build dependencies
FROM python:3.11 as builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
3. Production Patterns
3.1 Node.js Application
# Multi-stage Node.js build
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Build app
COPY . .
RUN npm run build
# Production image
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Result: 150MB vs 1GB single-stage
3.2 Go Application
# Multi-stage Go build
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Minimal runtime (scratch or alpine)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
# Result: 15MB vs 800MB
4. Advanced Techniques
4.1 Caching Build Dependencies
# Optimize layer caching
FROM python:3.11 as builder
# Install dependencies first (cached layer)
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# Copy code second (changes frequently)
COPY . .
# This way, dependency layer is cached unless requirements change
4.2 Multiple Builders
# Build frontend and backend separately
FROM node:20 AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
FROM golang:1.21 AS backend-builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o server
# Final runtime combines both
FROM alpine:latest
COPY --from=frontend-builder /app/frontend/dist /static
COPY --from=backend-builder /app/server /
CMD ["/server"]
5. Best Practices
- Use slim/alpine base images: python:3.11-slim vs python:3.11 saves 600MB
- Non-root user: Security best practice
- Clean up in same layer: RUN apt-get update && apt-get install && rm -rf /var/lib/apt/lists/*
- .dockerignore: Exclude unnecessary files
- Multi-platform builds: Support ARM and AMD64
6. Real-World Results
| Application | Single-Stage | Multi-Stage | Reduction |
|---|---|---|---|
| Python FastAPI | 1.2GB | 180MB | 85% |
| Node.js Express | 950MB | 150MB | 84% |
| Go Microservice | 800MB | 15MB | 98% |
7. Conclusion
Multi-stage Docker builds are essential for production. They reduce image sizes by 70-98%, improve security, and accelerate deployments. Every production Dockerfile should use multi-stage builds.
Written for DevOps engineers and developers deploying containerized applications.
Discover more from C4: Container, Code, Cloud & Context
Subscribe to get the latest posts sent to your email.