What Are Multi-Stage Builds?
Multi-stage builds let you use multiple FROM statements in a
single Dockerfile. Each FROM creates a new build stage, and you can selectively
copy artifacts from one stage to another. This dramatically reduces final image size by
excluding build tools and dependencies.
The Problem: Large Images
# β Single-stage build β includes build tools in production
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Final image includes: source code, devDependencies, build tools
# Size: ~1.2 GB π±
CMD ["npm", "start"]
The Solution: Multi-Stage Build
# β
Multi-stage build β clean production image
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
# Copy only what we need from the builder stage
COPY --from=builder /app/package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Final image: no source code, no devDeps, no build tools
# Size: ~150 MB β
React/Next.js Production Build
# Stage 1: Install dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Stage 2: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Stage 3: Production image
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create a non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built assets
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
Go Application β Extreme Size Reduction
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
# Stage 2: Minimal production image
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
# Final image: ~10 MB! (vs ~300 MB with golang base)
Python Application
# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Stage 2: Production
FROM python:3.12-slim
WORKDIR /app
# Copy installed packages from builder
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]
Building Specific Stages
# Build only up to a specific stage
docker build --target builder -t myapp:build .
# Build the production stage (default: last stage)
docker build -t myapp:prod .
# Use in CI/CD to run tests in the build stage
docker build --target test -t myapp:test .
Size Comparison
| Approach | Typical Size |
|---|---|
| Single-stage Node.js | ~1.2 GB |
| Multi-stage Node.js (Alpine) | ~150 MB |
| Single-stage Go | ~300 MB |
| Multi-stage Go (scratch) | ~10 MB |