Multi-stage builds create smaller, more secure Docker images. Here's how to use them effectively.
Why Multi-Stage Builds#
Single-stage problems:
- Build tools in production image
- Large image sizes
- Security vulnerabilities from dev dependencies
- Slow deployments
Multi-stage benefits:
- Separate build and runtime
- Smaller final images
- No build tools in production
- Better security posture
Basic Multi-Stage Build#
1# Stage 1: Build
2FROM node:20-alpine AS builder
3
4WORKDIR /app
5
6# Install dependencies
7COPY package*.json ./
8RUN npm ci
9
10# Build application
11COPY . .
12RUN npm run build
13
14# Stage 2: Production
15FROM node:20-alpine AS production
16
17WORKDIR /app
18
19# Copy only production dependencies
20COPY package*.json ./
21RUN npm ci --only=production
22
23# Copy built files from builder
24COPY /app/dist ./dist
25
26# Run as non-root user
27USER node
28
29EXPOSE 3000
30CMD ["node", "dist/index.js"]TypeScript Application#
1# Build stage
2FROM node:20-alpine AS builder
3
4WORKDIR /app
5
6COPY package*.json ./
7COPY tsconfig.json ./
8RUN npm ci
9
10COPY src ./src
11RUN npm run build
12
13# Production stage
14FROM node:20-alpine
15
16WORKDIR /app
17
18# Install production dependencies only
19COPY package*.json ./
20RUN npm ci --only=production && npm cache clean --force
21
22# Copy compiled JavaScript
23COPY /app/dist ./dist
24
25USER node
26EXPOSE 3000
27CMD ["node", "dist/index.js"]Next.js Application#
1# Dependencies stage
2FROM node:20-alpine AS deps
3WORKDIR /app
4COPY package*.json ./
5RUN npm ci
6
7# Builder stage
8FROM node:20-alpine AS builder
9WORKDIR /app
10COPY /app/node_modules ./node_modules
11COPY . .
12
13ENV NEXT_TELEMETRY_DISABLED 1
14RUN npm run build
15
16# Production stage
17FROM node:20-alpine AS runner
18WORKDIR /app
19
20ENV NODE_ENV production
21ENV NEXT_TELEMETRY_DISABLED 1
22
23RUN addgroup --system --gid 1001 nodejs
24RUN adduser --system --uid 1001 nextjs
25
26COPY /app/public ./public
27COPY /app/.next/standalone ./
28COPY /app/.next/static ./.next/static
29
30USER nextjs
31EXPOSE 3000
32ENV PORT 3000
33
34CMD ["node", "server.js"]Go Application#
1# Build stage
2FROM golang:1.21-alpine AS builder
3
4WORKDIR /app
5
6# Download dependencies
7COPY go.mod go.sum ./
8RUN go mod download
9
10# Build binary
11COPY . .
12RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server
13
14# Production stage - minimal image
15FROM scratch
16
17COPY /app/server /server
18COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
19
20EXPOSE 8080
21ENTRYPOINT ["/server"]Python Application#
1# Build stage
2FROM python:3.11-slim AS builder
3
4WORKDIR /app
5
6# Install build dependencies
7RUN apt-get update && apt-get install -y --no-install-recommends \
8 build-essential \
9 && rm -rf /var/lib/apt/lists/*
10
11# Create virtual environment
12RUN python -m venv /opt/venv
13ENV PATH="/opt/venv/bin:$PATH"
14
15# Install dependencies
16COPY requirements.txt .
17RUN pip install --no-cache-dir -r requirements.txt
18
19# Production stage
20FROM python:3.11-slim
21
22WORKDIR /app
23
24# Copy virtual environment
25COPY /opt/venv /opt/venv
26ENV PATH="/opt/venv/bin:$PATH"
27
28# Copy application
29COPY . .
30
31USER nobody
32EXPOSE 8000
33CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000"]Optimizing Layer Caching#
1FROM node:20-alpine AS builder
2
3WORKDIR /app
4
5# 1. Copy dependency files first (changes less often)
6COPY package*.json ./
7
8# 2. Install dependencies (cached if package.json unchanged)
9RUN npm ci
10
11# 3. Copy source code (changes most often)
12COPY tsconfig.json ./
13COPY src ./src
14
15# 4. Build
16RUN npm run build
17
18FROM node:20-alpine
19
20WORKDIR /app
21
22COPY package*.json ./
23RUN npm ci --only=production
24
25COPY /app/dist ./dist
26
27CMD ["node", "dist/index.js"]Using Build Arguments#
1FROM node:20-alpine AS builder
2
3ARG NODE_ENV=production
4ARG API_URL
5
6WORKDIR /app
7
8COPY package*.json ./
9RUN npm ci
10
11COPY . .
12
13# Use build args during build
14ENV NEXT_PUBLIC_API_URL=$API_URL
15RUN npm run build
16
17FROM node:20-alpine
18
19WORKDIR /app
20
21COPY /app/.next/standalone ./
22COPY /app/public ./public
23COPY /app/.next/static ./.next/static
24
25CMD ["node", "server.js"]# Build with arguments
docker build \
--build-arg API_URL=https://api.example.com \
-t myapp:latest .Testing Stage#
1FROM node:20-alpine AS base
2WORKDIR /app
3COPY package*.json ./
4RUN npm ci
5
6FROM base AS test
7COPY . .
8RUN npm run lint
9RUN npm run test
10
11FROM base AS builder
12COPY . .
13RUN npm run build
14
15FROM node:20-alpine AS production
16WORKDIR /app
17COPY /app/dist ./dist
18COPY /app/package*.json ./
19RUN npm ci --only=production
20CMD ["node", "dist/index.js"]# Run tests without building production image
docker build --target test .
# Build production image (includes test stage)
docker build --target production -t myapp:latest .Size Comparison#
1# Before: Single stage (~1.2GB)
2FROM node:20
3WORKDIR /app
4COPY . .
5RUN npm install
6RUN npm run build
7CMD ["node", "dist/index.js"]
8
9# After: Multi-stage (~150MB)
10FROM node:20-alpine AS builder
11WORKDIR /app
12COPY package*.json ./
13RUN npm ci
14COPY . .
15RUN npm run build
16
17FROM node:20-alpine
18WORKDIR /app
19COPY /app/dist ./dist
20COPY package*.json ./
21RUN npm ci --only=production
22CMD ["node", "dist/index.js"]Best Practices#
Structure:
✓ Use specific base image tags
✓ Order layers by change frequency
✓ Minimize layer count
✓ Use .dockerignore
Security:
✓ Run as non-root user
✓ Don't include secrets in image
✓ Scan images for vulnerabilities
✓ Use minimal base images
Performance:
✓ Leverage build cache
✓ Use --mount=type=cache for package managers
✓ Parallelize independent stages
✓ Clean up in same layer as install
Conclusion#
Multi-stage builds separate build-time dependencies from runtime, resulting in smaller, more secure images. Structure Dockerfiles to maximize cache hits, use Alpine or distroless base images, and always run as non-root in production.