Multi-stage Construction
An image shrinks from 1.2 GB to 12 MB—multi-stage builds are the most effective technique for reducing image size.
1. What You'll Learn
- Principles and Advantages of Multi-Stage Construction
- Copy artifacts from the build phase
- Select the optimal base image for operation
- Strategies for Reducing Image Size
- Cache Optimization and Debugging Techniques
2. A True Story from a Go Developer
(1) Pain Point: A 1.2 GB application image
Alice’s Go application image is 1.2 GB—because she used golang:1.22 as the base image, which includes the full Go compiler, source code, and toolchain. But the production environment only needs a 12 MB compiled binary. Each deployment takes 5 minutes to download the image, and the cloud server with 50 MB bandwidth slows down every time a release is made.
(2) A Multi-Stage Construction Approach
Bob taught her to use multi-stage builds: "In the first stage, use golang:1.22 to compile; in the second stage, copy the compiled output from Alpine."
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o server
# Stage 2: Run (only the binary, no Go compiler)
FROM alpine:3.19
COPY --from=builder /src/server /usr/local/bin/
CMD ["server"]
(3) Benefit: The image size was reduced from 1.2 GB to 12 MB
The image size was reduced by a factor of 100, deployment and download times were cut from 5 minutes to 5 seconds, and security vulnerabilities were reduced from over 200 to 10 (the compiler is not included in the production image).
3. Principles of Multi-Stage Construction
(1) Single-Stage vs. Multi-Stage Comparison
graph TB
subgraph Single["Single-stage Build"]
S1["golang:1.22<br/>800 MB base"] --> S2["go build<br/>Compilation Output"] --> S3["Final Image<br/>1.2 GB<br/>(Includes a compiler+Source Code)"]
end
subgraph Multi["Multi-stage Build"]
M1["Stage 1: golang:1.22<br/>800 MB (discarded)"] -->|"COPY --from"| M3["Stage 2: alpine:3.19<br/>7 MB base"]
M1 --> M2["go build<br/>Compilation Output"]
M2 -->|"Copy only the binary data"| M3
M3 --> M4["Final Image<br/>12 MB<br/>(Binary Only)"]
end
(1) Core Mechanism
| Stage | Purpose | Contents | Final Image |
|---|---|---|---|
| Builder Stage | Compilation/Build | Compiler, source code, build tools | ❌ Discard |
| Runner Stage | Execution | Copy only necessary artifacts | ✅ Retain |
4. Hands-On Multi-Stage Builds
▶ Example: Multi-stage builds in Go (Difficulty: ⭐⭐)
# ============================================
# Stage 1: Build the Go binary
# ============================================
FROM golang:1.22 AS builder
WORKDIR /src
# Copy dependency files first (cache optimization)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server
# ============================================
# Stage 2: Create minimal runtime image
# ============================================
FROM alpine:3.19
# Install CA certificates (for HTTPS requests)
RUN apk --no-cache add ca-certificates
# Copy only the compiled binary from builder
COPY --from=builder /src/server /usr/local/bin/server
# Run as non-root user
RUN adduser -D -u 1000 appuser
USER appuser
EXPOSE 8080
CMD ["server"]
# Build the multi-stage image
docker build -t go-app:1.0 .
# Check the final image size
docker images go-app:1.0
REPOSITORY TAG IMAGE ID SIZE
go-app 1.0 a1b2c3d4e5f6 12.5MB
▶ Example: Build with Node.js → Run with Nginx (Difficulty: ⭐⭐⭐)
Front-end projects such as React and Vue: Static files are generated during the build phase, and Nginx is used to serve HTTP requests during runtime.
# ============================================
# Stage 1: Build React application
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ============================================
# Stage 2: Serve with Nginx
# ============================================
FROM nginx:1.25-alpine
# Copy built assets from builder stage
COPY --from=builder /app/build /usr/share/nginx/html
# Custom Nginx config for SPA routing
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
node_modules might be 500+ MB, but the build output (HTML/CSS/JS) is usually only 5–20 MB.
▶ Example: Cloning a Python venv (Difficulty: ⭐⭐)
Python projects can also be built in multiple stages: the first stage creates a virtual environment, and the second stage simply copies the virtual environment and the application code.
# Stage 1: Build virtual environment
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install --no-cache-dir -r requirements.txt
# Stage 2: Run with venv only
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY . .
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python", "app.py"]
5. Selecting a Base Image to Run
(1) Comparison of the Images of Three Minimal Bases
| Base Image | Size | Shell Included | Package Manager Included | libc Included | Use Cases |
|---|---|---|---|---|---|
scratch |
0 MB | ❌ | ❌ | ❌ | Statically compiled Go/Rust binaries |
distroless |
2–20 MB | ❌ | ❌ | ✅ (glibc) | From Google, extremely secure |
alpine |
7 MB | ✅ (ash) | ✅ (apk) | ✅ (musl) | When debugging is required |
(2) Notes on the Scratch Image
# Scratch: absolutely nothing in the image
FROM scratch
COPY --from=builder /src/server /server
CMD ["/server"]
scratch image does not include a shell, ca-certificates, or time zone data. Go programs must be statically compiled CGO_ENABLED=0 to run on scratch. If your program requires HTTPS requests or timestamped logs, it is safer to use alpine.
6. Advanced Techniques for Multi-Stage Builds
▶ Example: Stage Naming and --target Build Selection (Difficulty: ⭐⭐)
# Named stages for clarity
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o server
# Alternative: development stage with hot-reload tools
FROM golang:1.22 AS dev
WORKDIR /src
RUN go install github.com/cosmtrek/air@latest
COPY . .
CMD ["air"]
# Production: minimal runtime
FROM alpine:3.19 AS prod
COPY --from=builder /src/server /usr/local/bin/
CMD ["server"]
# Build only the dev stage
docker build --target dev -t myapp:dev .
# Build only the prod stage
docker build --target prod -t myapp:prod .
▶ Example: --target for debugging intermediate artifacts (Difficulty: ⭐⭐⭐)
# Build and inspect the builder stage
docker build --target builder -t myapp:builder .
# Run the builder image to debug build issues
docker run -it myapp:builder bash
# Check the compiled binary
docker run --rm myapp:builder ls -la /src/server
7. Reference for Compilation Output Sizes by Language
| Language | Base Image | Build Output | Final Image | Reduction Ratio |
|---|---|---|---|---|
| growing: 1.22 (780 MB) | Static binary 12 MB | alpine: 12 MB | 65x | |
| Rust | rust:1.75 (1.5 GB) | Static binary 8 MB | alpine: 15 MB | 100x |
| Node.js | node:20 (1.1 GB) | build/ 5-20 MB | nginx-alpine: 25 MB | 44x |
| Java | eclipse-temurin:21 (450 MB) | JAR 30 MB | jre-alpine: 170 MB | 2.6x |
| Python | python:3.12 (1 GB) | venv/ 50 MB | slim: 200 MB | 5x |
8. Complete Example: Multi-Stage Build for a Go Web Application
# ============================================
# Multi-stage Dockerfile for Go web application
# Stage 1: Build with dependency caching
# Stage 2: Minimal Alpine runtime
# ============================================
# ---------- Stage 1: Builder ----------
FROM golang:1.22-alpine AS builder
# Install git (for go mod download with private repos)
RUN apk add --no-cache git
WORKDIR /src
# Cache: copy dependency files first
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build static binary (CGO_ENABLED=0 for scratch/alpine)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o /app/server
# ---------- Stage 2: Runtime ----------
FROM alpine:3.19
# Install runtime dependencies
RUN apk --no-cache add ca-certificates tzdata
# Create non-root user
RUN adduser -D -u 1000 appuser
WORKDIR /app
# Copy only the binary from builder
COPY --from=builder /app/server .
# Set ownership
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["/app/server"]
# Build
docker build -t go-web:1.0 .
# Run
docker run -d -p 8080:8080 --name go-web go-web:1.0
# Verify size and health
docker images go-web:1.0
docker inspect go-web --format='Size: {{.Size}}, Health: {{.State.Health.Status}}'
REPOSITORY TAG SIZE
go-web 1.0 15.2MB
Size: 15974400, Health: healthy
❓ FAQ
go build uses the cache directly. Only the final image does not include the Builder layers. Build speed is nearly the same as a single-stage build, but the image size is significantly reduced.--target builder to build up to the Builder stage, then use docker run -it myapp:builder bash to enter the debugging stage. You can also define a dev stage in the Dockerfile that includes debugging tools (such as dlv or air).docker exec to enter it. Debugging methods: ① Build a debug image using --target builder; ② Temporarily change FROM scratch to FROM alpine for debugging; ③ Use docker cp on the host to copy the log files out.docker history is still visible. Use --mount=type=secret (a BuildKit feature) to securely pass secrets: RUN --mount=type=secret,id=ssh_key cp /run/secrets/ssh_key /root/.ssh/id_rsa.📖 Summary
- Multi-stage build: Compilation occurs during the Builder stage, while the Runner stage only copies the artifacts, reducing the image size from gigabytes to megabytes
FROM xxx AS builderNaming phase,COPY --from=builderCross-phase replication- Base image options: scratch (0 MB) > distroless (2–20 MB) > alpine (7 MB)
--targetCan be built at a specified stage for debugging and development- Compiled languages like Go and Rust benefit the most (100x reduction in size), followed by Node.js front-end projects (44x)
CGO_ENABLED=0+-ldflags="-w -s"are the standard parameters for static compilation in Go
📝 Exercises
- Basic Exercise (Difficulty ⭐): Compare the sizes of Go application images built using single-stage and multi-stage builds—first use
FROM golang:1.22for a single-stage build, then use a multi-stage build, and record the difference in size between the two images. - Advanced Exercise (Difficulty ⭐⭐): Build a React application in multiple stages:
node:20-alpineBuild →nginx:1.25-alpineRun. Compare the final image sizes of the Node.js base image and the Nginx base image. - Challenge (Difficulty: ⭐⭐⭐): Use
docker historyto analyze the size of each layer in a multi-stage image build, and explain why the layers from the Builder stage are not included in the final image.



