Multi-stage Builds
Multi-stage builds allow you to use multiple FROM statements in your Dockerfile, each starting a new build stage. You can selectively copy artifacts from one stage to another, leaving behind everything you don't need in the final image. This dramatically reduces image size and improves security.
Why Multi-stage Builds?
Without multi-stage builds, you face a dilemma:
- Development image: Includes build tools, compilers, dev dependencies — large and insecure.
- Production image: Needs only the runtime and compiled artifacts — small and secure.
Previously, you needed separate Dockerfiles or complex scripts. Multi-stage builds solve this elegantly.
Size Comparison
| Approach | Image Size | Contains |
|---|---|---|
| Single-stage (with build tools) | 800+ MB | Build tools + runtime + app |
| Multi-stage (final stage only) | 50-100 MB | Runtime + app only |
| Multi-stage (distroless/scratch) | 5-20 MB | Minimal runtime + app |
Basic Multi-stage Build
dockerfile
# ============================================
# Stage 1: Build
# ============================================
FROM node:20 AS builder
WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Copy source and build
COPY . .
RUN npm run build
# ============================================
# Stage 2: Production
# ============================================
FROM node:20-alpine AS production
WORKDIR /app
# Copy only production dependencies
COPY package.json package-lock.json ./
RUN npm ci --only=production
# Copy built artifacts from the builder stage
COPY --from=builder /app/dist ./dist
# Run as non-root user
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]Build and Run
bash
# Build the multi-stage image
docker build -t my-app:latest .
# Only the final stage is included in the image
docker images my-app:latest
# Run the optimized image
docker run -d -p 3000:3000 my-app:latestLanguage-Specific Examples
Go Application
Go is ideal for multi-stage builds because it produces statically linked binaries:
dockerfile
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Build the binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o /app/server ./cmd/server
# Stage 2: Minimal runtime
FROM scratch
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy the binary
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]Result: Final image is typically 5-15 MB compared to 800+ MB with the Go build tools.
Java / Spring Boot Application
dockerfile
# Stage 1: Build with Maven
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]Python Application
dockerfile
# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libpq-dev && \
rm -rf /var/lib/apt/lists/*
# Create virtual environment and install deps
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim
# Copy the virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends libpq5 && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY . .
RUN adduser --system --no-create-home appuser
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:create_app()", "-b", "0.0.0.0:8000"]Rust Application
dockerfile
# Stage 1: Build
FROM rust:1.76 AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
# Stage 2: Minimal runtime
FROM debian:12-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/myapp
RUN useradd -r -s /bin/false appuser
USER appuser
EXPOSE 8080
CMD ["myapp"]Frontend Application (React/Vue/Angular)
dockerfile
# Stage 1: Build the frontend
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with Nginx
FROM nginx:1.25-alpine
# Copy custom nginx config
COPY nginx.conf /etc/nginx/nginx.conf
# Copy built static files from builder
COPY --from=builder /app/dist /usr/share/nginx/html
HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost/ || exit 1
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Advanced Patterns
Named Build Stages
dockerfile
FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
FROM base AS dependencies
RUN npm ci
FROM base AS production-dependencies
RUN npm ci --only=production
FROM dependencies AS test
COPY . .
RUN npm test
FROM dependencies AS build
COPY . .
RUN npm run build
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=production-dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]Building Specific Stages
bash
# Build only up to a specific stage
docker build --target test -t my-app:test .
docker build --target build -t my-app:build .
docker build --target production -t my-app:prod .
# Use in CI/CD: run tests first, then build production
docker build --target test -t my-app:test .
docker build --target production -t my-app:prod .Copying from External Images
dockerfile
FROM node:20-alpine
# Copy a binary from another image (not a build stage)
COPY --from=busybox:latest /bin/wget /usr/local/bin/wget
# Copy from a specific stage in another Dockerfile
COPY --from=my-other-image:latest /app/config /app/config
WORKDIR /app
COPY . .
CMD ["node", "server.js"]Parallel Build Stages
BuildKit can execute independent stages in parallel:
dockerfile
# These stages build in parallel
FROM node:20-alpine AS frontend-builder
WORKDIR /frontend
COPY frontend/ .
RUN npm ci && npm run build
FROM golang:1.22-alpine AS backend-builder
WORKDIR /backend
COPY backend/ .
RUN go build -o /backend/server .
# Final stage combines both
FROM alpine:3.19
COPY --from=frontend-builder /frontend/dist /app/public
COPY --from=backend-builder /backend/server /app/server
CMD ["/app/server"]┌─────────────────┐ ┌─────────────────┐
│ frontend-builder│ │ backend-builder │
│ (parallel) │ │ (parallel) │
└────────┬────────┘ └────────┬─────────┘
│ │
└───────────┬───────────┘
│
┌──────▼──────┐
│ Final Stage │
│ (combines) │
└─────────────┘Debugging Multi-stage Builds
bash
# Build and stop at a specific stage for debugging
docker build --target builder -t debug:latest .
docker run -it debug:latest /bin/sh
# View the build stages
docker build --progress=plain -t my-app:latest .
# List intermediate images
docker images --filter "dangling=true"Best Practices Summary
| Practice | Description |
|---|---|
| Name your stages | Use AS name for clarity |
| Order stages logically | Build → Test → Production |
| Copy only what's needed | Use specific COPY --from paths |
| Use smallest final base | Alpine, distroless, or scratch |
| Run tests in a stage | Catch issues before building production |
| Leverage parallel builds | Independent stages build simultaneously |
| Pin base image versions | Ensure reproducible builds |
| Use non-root users | In the final production stage |
Next Steps
- Image Building Best Practices — More image optimization techniques
- Build Optimization — Speed up your multi-stage builds
- BuildKit Guide — Advanced BuildKit features for multi-stage builds
- Build Cache — Optimize caching across stages
- Security Best Practices — Secure your multi-stage builds