The single highest-leverage decision for container security is the base image, because most of the CVEs a scanner reports live in OS packages you never chose to use — they came bundled with a full distribution. A stock ubuntu:22.04 carries 80 to 100 packages; a python:3.12 image weighs close to a gigabyte uncompressed. A minimal base image ships only your app, its runtime, and the handful of libraries it genuinely needs, which routinely removes 60 to 95% of OS-level findings simply because the vulnerable packages are no longer present. Chainguard scan comparisons have shown a python:3.12-slim image carrying well over a hundred known CVEs while an equivalent hardened minimal image reports zero at build time. This guide covers the main minimal-base options and how to build on each without giving up more than you have to.
The main minimal-base options
There is no single "most secure" base — the right one depends on your language and runtime.
scratch— literally empty. For fully static Go or Rust binaries. Zero OS attack surface.- Distroless (
gcr.io/distroless/*) — your runtime plus minimal libraries, no shell, no package manager. Language variants for Java, Python, Node.js. - Wolfi / Chainguard (
cgr.dev/chainguard/*) — a distro built specifically to produce minimal images with zero known CVEs at build time, updated continuously. - Alpine (
alpine:3.21) — ~7 MB, keeps a shell andapk, so it is minimal-ish but not shell-free. -slimvariants — a full distro with docs and extras removed. The easiest migration, the least reduction.
Building on a minimal base
Every minimal base assumes a multi-stage build: compile or install in a full-featured stage, then copy only the artifact into the minimal final stage.
# Go service on scratch
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server
FROM scratch
COPY --from=build /app/server /server
# scratch has no CA bundle — copy one if you make TLS calls
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/server"]
For interpreted languages, use the matching distroless runtime and keep the non-root variant.
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
FROM gcr.io/distroless/python3-debian12:nonroot
WORKDIR /app
COPY --from=build /deps /deps
COPY . .
ENV PYTHONPATH=/deps
USER 65532:65532
ENTRYPOINT ["main.py"]
Always pin the base by digest so a mutable tag cannot swap the image underneath you.
# Resolve a tag to its immutable digest before pinning
docker buildx imagetools inspect gcr.io/distroless/static-debian12:nonroot
Choosing a minimal base
| Base | Best for | Shell? | Notable tradeoff |
|---|---|---|---|
scratch | static Go / Rust | no | you supply CA certs, tzdata yourself |
| distroless | most compiled + interpreted apps | no | debug via ephemeral containers |
| Wolfi / Chainguard | teams wanting zero-CVE baselines | optional | some images behind a subscription |
| Alpine | small images needing a shell | yes | musl compatibility, apk present |
-slim | fastest migration | yes | smallest security gain |
A practical migration order keeps the operational risk low: start with stateless, statically linked services that map cleanly onto scratch or distroless/static, then move interpreted-language services onto their matching distroless runtime, validating startup, health checks, and logging at each step. Update runbooks to use ephemeral debug containers before you cut over on-call teams, and pin every base by digest so a rollback is deterministic. Rebuild on a weekly schedule even when your code has not changed, so you inherit upstream patches rather than freezing a base at the version you first pulled.
How Safeguard scans your images
Minimal bases shrink the OS-package problem but not the vulnerabilities in your application's own dependencies, which live in the app layer no matter how small the base is. Safeguard scans both, reading components directly from filesystem layers and language manifests so it inventories even a scratch image with no package manager to query. It runs reachability analysis so the findings you triage are the ones exploitable from code your service actually calls, emits a CycloneDX or SPDX SBOM per build, and opens auto-fix pull requests when a base-image bump or dependency patch resolves a reachable finding. Run it in CI with the Safeguard CLI, gate pull requests with Secure Containers, and cover the dependency tree with software composition analysis. You can start on a single repository from pricing that begins at a dollar.
Frequently Asked Questions
Which minimal base should I default to?
Distroless is the safe default for most services: no shell, no package manager, and language-specific variants that need minimal changes. Use scratch only for fully static binaries, and consider Wolfi/Chainguard if you want a continuously rebuilt zero-CVE baseline.
Do minimal images break TLS or timezones?
They can. scratch and some distroless images do not include a CA certificate bundle or timezone data, so HTTPS calls or local-time formatting fail until you copy ca-certificates.crt and tzdata in from the build stage. Distroless base variants include the CA bundle.
Is a minimal base a replacement for scanning?
No. It reduces how many OS CVEs you start with, but your application dependencies — the npm, PyPI, or Maven packages you pull in — ship regardless of base. Log4Shell rode into production inside application layers on every base image. Minimize and scan.
How do I debug a minimal image in production?
Use an ephemeral debug container that brings its own tools, such as kubectl debug -it pod --image=busybox --target=app, rather than adding a shell to the runtime image. Many distroless images also publish a :debug tag for local troubleshooting.
Want to see how much a minimal base cuts your CVE count? Connect a repository at app.safeguard.sh/register, and read the base-image scanning guide in the documentation at docs.safeguard.sh.