The security difference between base images is mostly a difference in package count: Ubuntu 24.04 ships around 100 packages, Alpine 3.21 ships about 15, and a distroless static image ships effectively zero — and CVE exposure tracks package count almost linearly. Everything you can't remove, you have to patch, scan, and explain to auditors forever. So the base image decision is really a decision about how much operating system you want to be responsible for.
The numbers, side by side
Pull the three images and scan them with the same tool on the same day and you get something like this (Trivy 0.58, January 2026, representative run — your date will vary):
| Image | Size | OS packages | Typical open CVEs | Shell? | Package manager? |
|---|---|---|---|---|---|
ubuntu:24.04 | 78 MB | ~100 | 15–40 (mostly low/medium) | yes | apt |
alpine:3.21 | 7.8 MB | ~15 | 0–3 | yes (busybox) | apk |
gcr.io/distroless/static-debian12 | ~2 MB | 3 (ca-certs, tzdata, base-files) | usually 0 | no | no |
gcr.io/distroless/cc-debian12 | ~24 MB | ~8 (adds glibc, libssl) | 0–5 | no | no |
The command to reproduce:
trivy image --severity HIGH,CRITICAL ubuntu:24.04
trivy image --severity HIGH,CRITICAL alpine:3.21
trivy image --severity HIGH,CRITICAL gcr.io/distroless/static-debian12
Two honest caveats about that table. First, Ubuntu's CVE count is inflated by Canonical's policy of tracking low-severity issues it hasn't prioritized fixing; many of those findings are real-but-dormant. Second, a zero-CVE scan of distroless doesn't mean zero risk — it means near-zero scannable OS surface, which is not the same thing. Your application layer and its dependencies dominate the actual risk either way, which is why base image choice complements, not replaces, SCA on your application dependencies.
Ubuntu: the comfortable default with a patch tax
Ubuntu (and Debian, same analysis) gives you glibc, coreutils, a real shell, apt, and compatibility with essentially every binary and installation script ever written. Operationally it's frictionless.
The costs are ongoing rather than upfront. You inherit ~100 packages worth of CVE stream, so your scanner output is permanently noisy and your registry rebuild cadence has to be tight — Canonical patches fast, but the patches only reach you when you rebuild. Teams that build on ubuntu:24.04 and rebuild monthly are typically carrying 20+ known-fixed CVEs at any moment purely from staleness.
If you need Ubuntu compatibility, the mitigations are mechanical: rebuild weekly, pin by digest with automated bumps (we covered the pattern in digests vs tags), and run apt-get upgrade -y in the Dockerfile so patches don't wait for a base tag refresh.
Alpine: small, fast, and musl-shaped
Alpine's ~8 MB footprint comes from busybox and musl libc. Fifteen-ish packages means the CVE stream is a trickle, and apk upgrades are quick. For Go, Rust, and most Node workloads, it's a fine default.
The tradeoffs are specific and worth knowing before you commit:
- musl is not glibc. Anything shipping precompiled glibc binaries — some Python wheels, Oracle clients, certain ML libraries — either needs a musl build or won't run. Python teams pay this tax most often: wheels without musllinux builds fall back to compiling from source, turning a 40-second image build into a 12-minute one.
- DNS behavior differs. musl's resolver historically handled some search-domain and TCP-fallback cases differently than glibc, which shows up as flaky service discovery in Kubernetes. Most of this was fixed by musl 1.2.4 (Alpine 3.18+), but it burned enough teams to earn a permanent reputation.
- Security tooling coverage is good now but Alpine's
apkdatabase gets slightly less scanner attention than Debian's dpkg; false negatives are rarer than they were in 2021, not extinct.
We've published a deeper head-to-head in Alpine vs Debian base image security if you're choosing between those two specifically.
Distroless: no shell, no package manager, no excuses
Google's distroless images (and the same-idea Chainguard Images, built on Wolfi) remove the operating system's interactive surface entirely. No /bin/sh, no apk/apt, no busybox. An attacker who achieves code execution in your container has no curl to pull a second stage, no shell for a reverse connection, no package manager to install one. It doesn't stop exploitation, but it meaningfully raises the cost of everything after exploitation.
The build pattern is a multi-stage Dockerfile:
FROM golang:1.23 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/api
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app /app
ENTRYPOINT ["/app"]
Note the :nonroot tag — you get a non-root user for free, which closes another finding class.
The costs: debugging changes shape. kubectl exec -it pod -- sh stops working, by design. The replacement is ephemeral debug containers:
kubectl debug -it mypod --image=busybox --target=api
That shares the process namespace with your distroless container, so you can inspect it without shipping a shell in production. There's also a :debug variant of every distroless image with busybox included — fine for staging, a policy violation in prod. For interpreted runtimes (Python, Node), distroless works but you lose pip/npm at runtime, which is honestly a feature: images that can't mutate themselves are images whose SBOM stays true.
A decision table that survives contact with reality
| If... | Use |
|---|---|
| Static Go/Rust binary | distroless/static or Chainguard static — no contest |
| Compiled binary needing glibc/libssl | distroless/cc or distroless/base |
| Node/Python, team comfortable with debug containers | distroless runtime variants or Chainguard |
| Python with heavy native wheels | python:3.12-slim (Debian) — skip Alpine's musl tax |
| Legacy app, vendor scripts, "must have a shell" | ubuntu:24.04 + weekly rebuilds + digest pinning |
The pattern across teams I've worked with: moving a fleet's default from Ubuntu to distroless-or-slim cuts OS-level findings by 80–95% and, more importantly, cuts triage time to near zero because the residual findings are actually worth reading. That reclaimed attention is the real security win.
Frequently asked questions
Is distroless actually more secure or just quieter in scanners?
Both, and the two compound. Fewer packages means fewer real vulnerabilities (not just fewer findings), and removing the shell and package manager removes post-exploitation tooling attackers rely on. The quieter scanner output is a second-order benefit: your team stops tuning out results.
Does Alpine's musl libc create security problems?
musl itself has a solid security record — its issues are compatibility, not vulnerability. The security-relevant risk is indirect: teams hitting musl incompatibilities sometimes bolt glibc compatibility shims (gcompat, libc6-compat) onto Alpine, ending up with a hybrid that gets less scrutiny than either mainstream option. If you need glibc, use a glibc-based image.
How do I debug a distroless container in production?
Use kubectl debug with an ephemeral container targeting the pod's process namespace, which attaches tooling without modifying the running image. For non-Kubernetes hosts, docker debug (Docker Desktop 4.27+) does the same. Keep the :debug image variants out of production admission policy.
Do I still need dependency scanning with a minimal base image?
Yes — more than ever, proportionally. With the OS surface near zero, essentially 100% of your remaining vulnerability exposure lives in application dependencies, which is exactly what SBOM-driven tooling like SBOM Studio tracks. A distroless image running a vulnerable log4j was the canonical 2021 counterexample to "small image, no problem."