Docker Multi-Stage Builds: Optimizing Production Images
· ~1 min readDevOpsDocker
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"]
```text
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
```dockerfile
# 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"]
```text
Result: **180 MB image** - 85% smaller.
## Pattern Library
### Go Application
```dockerfile
# 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"]
```text
Result: **~10 MB image**
### React Application
```dockerfile
# 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;"]
```text
### Python with Virtual Environment
```dockerfile
# 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"]
```text
## Security Benefits
### Fewer Vulnerabilities
```bash
# Large image
docker scout quickview myapp:v1
# 47 CVEs detected
# Multi-stage image
docker scout quickview myapp:v2
# 3 CVEs detected
```text
### No Build Tools
Attackers can't compile exploits if there's no compiler:
```dockerfile
# Runtime has no gcc, make, or build tools
FROM alpine:3.18
# Only what's needed to run
```text
## Best Practices
### Use Specific Base Images
```dockerfile
# Bad: floating tag
FROM node:18
# Good: pinned digest
FROM node:18.19.0-alpine3.19@sha256:abc123...
```text
### Minimize Layers
```dockerfile
# 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/*
```text
### Use .dockerignore
```text
node_modules
npm-debug.log
Dockerfile
.dockerignore
.git
.gitignore
README.md
.env
coverage
.nyc_output
```text
### Non-Root User
```dockerfile
FROM node:18-alpine
RUN addgroup -g 1001 -S appgroup && \
adduser -u 1001 -S appuser -G appgroup
USER appuser
```text
## 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.