25 / 75 · 08 Infrastructure as Code Testing · Minimal Container Images← prev⊞ allnext →☰ Read as one page
5.3Multi-Stage Builds: The Foundation
Multi-stage builds separate the build environment from the runtime environment. Your build stage can have compilers, package managers, and development tools. Your runtime stage has only the compiled output.
Bad: Single-Stage Build
# BAD: 900MB+ image with shell, package managers, compilers
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# This image contains:
# - npm (can be used to install malware)
# - apt-get (can install arbitrary packages)
# - bash, sh (can run arbitrary commands)
# - gcc, make (compilation tools)
# - wget, curl (can download payloads)
# - /etc/passwd, /etc/shadow (user database)
Good: Multi-Stage Build
# GOOD: Multi-stage build, ~150MB distroless image
# Stage 1: Build
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["dist/server.js"]
# No shell, no package manager, no curl -- minimal attack surface
Python Multi-Stage Build
# Stage 1: Build with pip and compilation tools
FROM python:3.12-slim AS builder
WORKDIR /app
RUN pip install --no-cache-dir poetry
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt -o requirements.txt
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY . .
# Stage 2: Runtime with only the installed packages
FROM python:3.12-slim
WORKDIR /app
# Copy only the installed packages from the builder
COPY --from=builder /install /usr/local
COPY --from=builder /app .
# Security hardening
RUN useradd --create-home --shell /bin/false appuser
USER appuser
CMD ["python", "-m", "app.main"]
Go Application: scratch Image
# Go produces static binaries -- no runtime needed
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Build a statically linked binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server .
# The scratch image is literally empty -- 0 bytes
FROM scratch
# Copy CA certificates for HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
# Total image size: ~15MB (just the Go binary + certs)