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:

  1. Reducing attack surface: Fewer packages in production images
  2. Separating secrets: Build-time secrets don’t leak to production
  3. Minimal base images: Using Alpine or distroless images
  4. 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.