Skip to content

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

ApproachImage SizeContains
Single-stage (with build tools)800+ MBBuild tools + runtime + app
Multi-stage (final stage only)50-100 MBRuntime + app only
Multi-stage (distroless/scratch)5-20 MBMinimal 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:latest

Language-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

PracticeDescription
Name your stagesUse AS name for clarity
Order stages logicallyBuild → Test → Production
Copy only what's neededUse specific COPY --from paths
Use smallest final baseAlpine, distroless, or scratch
Run tests in a stageCatch issues before building production
Leverage parallel buildsIndependent stages build simultaneously
Pin base image versionsEnsure reproducible builds
Use non-root usersIn the final production stage

Next Steps

基于 MIT 许可发布