Stage 4 · Provision
Building Images
Multi-Stage Builds
Compile Go, Node.js, or Java artifacts in builder stages and copy only runtime files forward.
Why Multi-Stage?
A single-stage Dockerfile that builds and runs your application ends up containing compilers, build tools, source code, and test frameworks in the final image. Multi-stage builds let you separate the build environment from the runtime environment, producing minimal production images.
Basic Pattern
A multi-stage Dockerfile has multiple FROM instructions. Each FROM starts a new stage. You can COPY artifacts from one stage to another using the --from flag. Only the final stage becomes the image.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# Stage 2: Run
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]The builder stage contains the full Node.js toolchain and source code. The final stage copies only the compiled output and production dependencies. The build tools are left behind.
Node.js Example
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]Three stages: deps installs all dependencies, build compiles TypeScript, and the final image contains only node_modules, dist, and the runtime. The TypeScript compiler and source code are excluded.
Go Example
Go compiles to a static binary, making it ideal for multi-stage builds. The final image can be based on scratch (empty) or distroless, producing a 5-15MB container.
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server ./cmd/server
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]The builder stage compiles the Go binary. The final stage uses scratch — the smallest possible base. No shell, no package manager, no unnecessary files. Just the binary and CA certificates for HTTPS.
If your application compiles to a static binary (Go with CGO_ENABLED=0, Rust with musl, or a statically linked C program), use scratch as the final base. The image will be only as large as your binary plus any required files.
Java Example
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY gradle/ ./
COPY *.gradle ./
RUN gradle dependencies
COPY src/ src/
RUN gradle installDist
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=builder /app/build/install/myapp ./myapp
EXPOSE 8080
CMD ["./myapp/bin/myapp"]The build stage uses the full JDK to compile. The runtime stage uses JRE only. For even smaller images, use jlink to create a custom JRE with only the modules your application needs.
Named Stages
Name your build stages with AS to make COPY --from references clear and maintainable. You can reference stages by name or by index (0, 1, 2). Named stages improve readability and allow you to reorder stages without breaking references.
FROM node:20-alpine AS deps
# ...
FROM node:20-alpine AS build
COPY --from=deps /app/node_modules ./node_modules
# ...
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=build /app/dist ./dist
CMD ["dist/server.js"]Named stages make the Dockerfile self-documenting. The COPY --from=deps clearly communicates that you are copying from the dependency installation stage. This is much clearer than COPY --from=0.
Intermediate stages are discarded after the build. Only the last FROM instruction produces the final image. If you need artifacts from an earlier stage, COPY them forward with --from.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.