L16. Minimal Base Images: Distroless, Scratch and Chainguard
Video generating
Check back soon for the video lesson on Minimal Base Images: Distroless, Scratch and Chainguard
Every package in your container image is attack surface. Learn how to reduce image size and vulnerability count by using distroless, scratch, and Chainguard base images.
The Attack Surface Problem
A typical Ubuntu or Debian base image contains hundreds of packages: shells, package managers, compilers, utilities. Most of these are never used by your application but each one can contain vulnerabilities.
A standard python:3.12 image has 400+ packages and 100+ known vulnerabilities. A distroless equivalent has 20 packages and near-zero vulnerabilities.
Distroless Images
Google's distroless images contain only your application runtime and its dependencies. No shell, no package manager, no utilities:
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
WORKDIR /app
CMD ["main.py"]
Without a shell, an attacker who gains code execution cannot easily explore the filesystem, download tools, or establish a reverse shell.
Scratch Images
scratch is an empty image with literally nothing in it. It works for statically compiled binaries (Go, Rust):
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /server .FROM scratch
COPY --from=builder /server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]
Scratch images have zero packages and zero vulnerabilities. The tradeoff: no shell for debugging and no DNS resolver (your application must handle DNS itself or copy the resolver libraries).
Chainguard Images
Chainguard provides hardened, minimalist images built with Wolfi (an undistro designed for containers). They include daily vulnerability patching and are signed with Sigstore:
FROM cgr.dev/chainguard/python:latest
COPY --chown=nonroot:nonroot . /app
WORKDIR /app
CMD ["main.py"]Chainguard images run as non-root by default and include SBOMs. They cover most popular runtimes: Python, Node.js, Java, Go, .NET, and nginx.
Comparison
| Base Image | Packages | CVEs (typical) | Shell | Size |
|---|---|---|---|---|
| ubuntu:24.04 | 200+ | 50-100 | Yes | 78 MB |
| python:3.12-slim | 100+ | 20-40 | Yes | 130 MB |
| distroless/python3 | 20 | 0-5 | No | 50 MB |
| chainguard/python | 15 | 0-2 | No | 45 MB |
| scratch | 0 | 0 | No | 0 MB |
Multi-Stage Builds
All minimal image strategies use multi-stage builds:
- Builder stage: Install dependencies, compile code, run tests
- Runtime stage: Copy only the application artifacts into a minimal base image
This separation ensures build tools (compilers, package managers, test frameworks) never appear in the production image. Best practice: Pin base image versions to a specific digest rather than a mutable tag to prevent supply chain attacks through tag manipulation:
FROM cgr.dev/chainguard/python:latest@sha256:abc123...- ✓Every package in a container image is attack surface: minimal images drastically reduce vulnerability counts
- ✓Distroless images contain only the application runtime with no shell, package manager, or utilities
- ✓Scratch images are completely empty and work for statically compiled binaries (Go, Rust)
- ✓Chainguard images are hardened, run as non-root by default, include SBOMs, and receive daily vulnerability patches
- ✓Pin base image versions to a specific digest (not a mutable tag) to prevent supply chain attacks through tag manipulation
1. Why is the absence of a shell in distroless images a security benefit?
2. What type of applications can use scratch (empty) base images?
3. Why should you pin base images by digest instead of tag?