Optimizing Docker Images with Multi-Stage Builds
Docker images can quickly balloon in size if you’re not careful. Large images lead to slower deployments, increased bandwidth costs, and potential security vulnerabilities. Multi-stage builds are a powerful technique to create slim, production-ready images.
The Problem with Traditional Dockerfiles
Consider a typical Node.js application Dockerfile:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]
This approach has several issues:
- Large base image: The full Node.js image includes build tools you don’t need in production
- Unnecessary dependencies: Dev dependencies remain in the final image
- Build artifacts: Source files and build cache bloat the image
- Security surface: More packages mean more potential vulnerabilities
Enter Multi-Stage Builds
Multi-stage builds allow you to use multiple FROM statements in your Dockerfile. Each FROM starts a new stage, and you can selectively copy artifacts from one stage to another.
Example: Optimized Node.js Application
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Copy only necessary files
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package.json ./
USER nodejs
EXPOSE 3000
CMD ["node", "dist/index.js"]
Results
- Before: 1.2GB
- After: 180MB
- Reduction: 85%
Multi-Stage Build Patterns
Pattern 1: Separate Build and Runtime
Perfect for compiled languages (Go, Rust, C++):
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main .
# Runtime stage
FROM alpine:latest
COPY --from=builder /app/main /usr/local/bin/
CMD ["main"]
Pattern 2: Development vs Production
Maintain a single Dockerfile for multiple environments:
# Base stage
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
# Development stage
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Production stage
FROM base AS production
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["npm", "start"]
Build for development:
docker build --target development -t myapp:dev .
Build for production:
docker build --target production -t myapp:prod .
Pattern 3: Test Stage
Include testing in your build pipeline:
FROM node:20-alpine AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM dependencies AS test
COPY . .
RUN npm test
FROM dependencies AS production
COPY --from=test /app/dist ./dist
CMD ["node", "dist/index.js"]
Best Practices
1. Use Specific Base Images
# ❌ Bad
FROM node
# ✅ Good
FROM node:20-alpine
2. Order Layers by Change Frequency
# Dependencies change less frequently
COPY package*.json ./
RUN npm ci
# Source code changes more frequently
COPY . .
3. Minimize Layer Count
# ❌ Bad - 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# ✅ Good - 1 layer
RUN apt-get update && \
apt-get install -y curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
4. Use .dockerignore
Create a .dockerignore file:
node_modules
npm-debug.log
.git
.gitignore
README.md
.env
dist
coverage
5. Run as Non-Root User
RUN addgroup -g 1001 -S appgroup && \
adduser -S appuser -u 1001
USER appuser
Security Benefits
Multi-stage builds improve security by:
- Reducing attack surface: Fewer packages in production images
- Separating secrets: Build-time secrets don’t leak to production
- Minimal base images: Using Alpine or distroless images
- No build tools: Compilers and dev tools stay in build stage
Conclusion
Multi-stage Docker builds are essential for modern containerized applications. They provide:
- Dramatically smaller image sizes
- Faster deployments
- Improved security
- Better separation of concerns
- Cost savings on storage and bandwidth
Start implementing multi-stage builds in your projects today, and you’ll see immediate benefits in image size, security, and deployment speed.