03. Multi-Stage Builds
Multi-stage builds solve the problem of delivering production images that contain only runtime necessities while preserving build capabilities. The pattern uses multiple FROM statements, each beginning a new stage with independent base image.
Stage naming clarifies the build topology. The builder stage compiles source code. The runtime stage receives the compiled output. The final stage represents what gets shipped to production.
Common patterns exist for different language ecosystems. Go compiles to a static binary requiring only the operating system runtime. Python requires the interpreter and dependencies throughout execution. Rust produces native binaries with optional dynamic linking.
The COPY --from syntax transfers artifacts between stages. Copying specific paths rather than entire directories keeps final images focused. The destination path in the final stage should match expected runtime paths.
Build arguments enable cross-stage communication. SOURCE_STAGE_NAME and FINAL_STAGE_NAME expose stage indices for environments without named stage support.
# Syntax version required for advanced features
# syntax=docker/dockerfile:1.4
FROM golang:1.22-alpine AS builder
WORKDIR /src
# Dependencies first for layer caching
COPY go.mod go.sum ./
RUN go mod download
# Source after dependencies
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-w -s" \
-o /bin/inference-server \
./cmd/server
# Runtime stage: minimal surface area
FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /bin/inference-server /bin/inference-server
ENTRYPOINT ["/bin/inference-server"]
The scratch base image provides literally nothing beyond the kernel interface. Applications requiring TLS certificates, locale data, or DNS resolution need those files copied explicitly. Applications requiring static linking must compile without CGO.
Build secrets keep sensitive credentials out of image layers. dockerfile --secret passes secrets to builds without storing them in cache or layers. The RUN --mount=type=secret syntax provides temporary mounts that disappear after instruction completion.
Convert an existing Dockerfile for a Node.js inference service into a multi-stage build. Identify build-time dependencies (TypeScript compiler, node-gyp build tools) versus runtime dependencies (Node interpreter, production packages). Verify the final image contains no compiler toolchains or source maps.
FROM node:20-alpine AS builder
WORKDIR /app
# Install all dependencies including dev dependencies
COPY package*.json ./
RUN npm ci
# Copy and build TypeScript
COPY . .
RUN npm run build
# Production stage: strip dev dependencies
FROM node:20-alpine AS runtime
WORKDIR /app
# Copy only production dependencies and built output
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]