Tips and Tricks – Use Multi-Stage Docker Builds for Smaller Images

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.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.