Multi-Stage Builds
What Are Multi-Stage Builds?
Multi-stage builds keep your final image small by separating the build environment from the runtime environment. This is critical for production deployments — you get the tools you need to compile or bundle your code, then copy only the final artifact into a minimal runtime image.
Why Multi-Stage Builds Matter
| Without Multi-Stage | With Multi-Stage |
|---|---|
| Build tools in final image | Only runtime artifacts |
| Large image (hundreds of MB) | Small image (often < 20MB) |
| Larger attack surface | Minimal attack surface |
| Slower pull times | Fast pulls and deployments |
How It Works
┌─────────────────────────────┐
│ Stage 1: BUILD │
│ ┌───────────────────────┐ │
│ │ Full toolchain │ │
│ │ Source code │ │
│ │ Dependencies │ │
│ │ Compile/Build step │ │
│ │ ↓ │ │
│ │ Binary / Bundle │──┼──→ COPY --from=builder
│ └───────────────────────┘ │
│ ❌ Discarded │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Stage 2: RUNTIME │
│ ┌───────────────────────┐ │
│ │ Minimal base image │ │
│ │ + copied artifact │ │
│ │ = Final image ✅ │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
Go Application — Multi-Stage Build
Go compiles to a single static binary, making it the perfect candidate for multi-stage builds.
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server .
# Stage 2: Runtime (tiny final image)
FROM alpine:3.19
COPY --from=builder /server /server
EXPOSE 8080
CMD ["/server"]
Size comparison:
| Stage | Base Image | Approximate Size |
|---|---|---|
| Build | golang:1.22 |
~800 MB |
| Final | alpine:3.19 |
~8 MB (binary + Alpine) |
The build stage uses the full Go toolchain (~800MB). The final image uses Alpine (~5MB) with just the compiled binary, resulting in a final image under 20MB.
Using scratch for Even Smaller Images
For Go, you can use scratch (an empty image) since Go produces fully static binaries:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /server .
FROM scratch
COPY --from=builder /server /server
EXPOSE 8080
CMD ["/server"]
This produces an image that is only the size of your binary — often 5-15MB.
Note: scratch has no shell, no package manager, no debugging tools. Use it only when you’re confident the binary is fully self-contained.
Node.js — Multi-Stage Build
Node.js apps benefit from multi-stage builds to separate dependency installation from runtime:
# Stage 1: Install dependencies
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER 1000
EXPOSE 3000
CMD ["node", "server.js"]
Why Separate Dependency Installation?
- Caching —
npm cionly reruns whenpackage.jsonorpackage-lock.jsonchange - Cleaner image — no leftover build artifacts, npm cache, or devDependencies
- Reproducibility —
npm ciuses the lockfile for deterministic installs
Java — Multi-Stage Build
Java applications need the JDK to compile but only the JRE to run:
# 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 with JRE only
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
USER 1000
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
| Stage | Image | Purpose |
|---|---|---|
| Builder | maven:3.9-eclipse-temurin-21 (~500MB) |
Full JDK + Maven |
| Runtime | eclipse-temurin:21-jre-alpine (~100MB) |
JRE only |
Python — Multi-Stage Build
Python doesn’t compile to a binary, but multi-stage builds still help to keep the final image clean:
# Stage 1: Build wheels
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
USER 1000
EXPOSE 8080
CMD ["python", "app.py"]
Key Syntax: COPY –from
The COPY --from=<stage> instruction is what makes multi-stage builds work:
# Copy from a named stage
COPY --from=builder /app/dist ./dist
# Copy from a stage by index (0-based)
COPY --from=0 /app/build ./build
# Copy from an external image
COPY --from=nginx:1.27-alpine /etc/nginx/nginx.conf /nginx.conf
CKAD relevance: While the exam focuses on Kubernetes, understanding multi-stage builds helps you reason about image sizes, security, and efficient Pod deployments.
Summary
| Language | Build Stage | Runtime Stage | Typical Savings |
|---|---|---|---|
| Go | golang:1.22 (800MB) |
alpine or scratch (5-15MB) |
98% |
| Node.js | node:18-alpine (180MB) |
node:18-alpine (lean) |
30-50% |
| Java | maven + JDK (500MB) |
JRE-alpine (100MB) |
80% |
| Python | python:3.12-slim (150MB) |
python:3.12-slim (lean) |
20-40% |