Docker Multi-Stage Builds: Optimizing Production Images
DevOpsDocker
dockercontainersoptimizationsecurity
Docker Multi-Stage Builds: Optimizing Production Images
Multi-stage builds are the single most impactful optimization for Docker images. They separate build dependencies from runtime, producing minimal, secure production images.
The Problem
A typical Node.js Dockerfile:
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
Result: 1.2 GB image containing:
- Full Node.js toolchain
- npm cache
- devDependencies
- TypeScript compiler
- Source maps
- Build artifacts
None of this is needed at runtime.
Multi-Stage Solution
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production=false
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
CMD ["node", "dist/index.js"]
Result: 180 MB image - 85% smaller.
Pattern Library
Go Application
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
# Runtime stage
FROM scratch
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
Result: ~10 MB image
React Application
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage (nginx)
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Python with Virtual Environment
# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Runtime stage
FROM python:3.11-slim
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
CMD ["python", "app.py"]
Security Benefits
Fewer Vulnerabilities
# Large image
docker scout quickview myapp:v1
# 47 CVEs detected
# Multi-stage image
docker scout quickview myapp:v2
# 3 CVEs detected
No Build Tools
Attackers can't compile exploits if there's no compiler:
# Runtime has no gcc, make, or build tools
FROM alpine:3.18
# Only what's needed to run
Best Practices
Use Specific Base Images
# Bad: floating tag
FROM node:18
# Good: pinned digest
FROM node:18.19.0-alpine3.19@sha256:abc123...
Minimize Layers
# Bad: multiple layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean
# Good: single layer
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
Use .dockerignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
coverage
.nyc_output
Non-Root User
FROM node:18-alpine
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
USER appuser
Size Comparison
| Approach | Size | Security |
|---|---|---|
| Single stage | 1.2 GB | 47 CVEs |
| Multi-stage | 180 MB | 3 CVEs |
| Multi-stage + distroless | 80 MB | 0 CVEs |
Conclusion
Multi-stage builds should be the default for all production Dockerfiles. They reduce size, improve security, and enforce clean separation between build and runtime environments.