Week 7 - Multi-Stage Builds, Distroless, Minimal Images¶
7.1 Conceptual Core¶
- The point of a build image is to not be the runtime image. A modern image pipeline:
- Stage 1 (build): full build environment (compiler, headers, dev tools).
- Stage 2 (test): the build artifacts plus test runners.
- Stage 3 (runtime): a minimal image with just the artifact.
- Distroless images (Google's
gcr.io/distroless/*) contain only the runtime dependencies-no shell, no package manager, nocat. Smaller attack surface, smaller image, faster startup. - Static binaries (Go with
CGO_ENABLED=0, Rust with musl, Java GraalVM native-image) can run onscratch(the empty base image): typically <20 MB total.
7.2 Mechanical Detail¶
- Multi-stage Dockerfile:
- Distroless variants:
static,base,cc,python3,java, etc. Pick the smallest that works. - The
nonroottag ensures the default user is UID 65532-never root. - The
:debugtag adds busybox for emergency debugging-use only for one-off triage in dev.
7.3 Lab-"Three Image Diet"¶
Take a Go (or Rust, or Python) service and produce three images:
1. Naive: FROM ubuntu, build inline. Measure size.
2. Distroless: multi-stage with gcr.io/distroless/static. Measure size.
3. Scratch: static build, FROM scratch. Measure size.
Document the size delta and any operational tradeoffs (e.g., scratch has no ca-certificates -tls.Configfailures unless youCOPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/`).
7.4 Hardening Drill¶
- Run
docker scout cves(ortrivy image) on each variant; observe that scratch and distroless have ~zero CVEs from the base, while ubuntu/alpine have many. The CVEs aren't gone-the attack surface is reduced. Internalize the difference.
7.5 Production Readiness Slice¶
- Configure your CI to fail builds whose image grows by >5% vs the baseline. This forces conscious deltas; surprise growth is often a leaked dev tool.