What Standard Container Images Actually Include
A standard container image is deceptively heavy. When you start with ubuntu:22.04 or debian:bookworm, you get far more than just the operating system kernel calls. You receive an entire userland: package managers, shells, utilities, debugging tools, and libraries for operations that have nothing to do with your application.
An ubuntu:22.04 image weighs roughly 77 MB compressed, but expanded on disk it occupies 200+ MB. More concerning than size is what comes along for the ride. The image includes apt, dpkg, /bin/bash, curl, wget, tar, gzip, grep, sed, awk, find, ps, and hundreds of other system utilities. Each utility brings dependencies. Each dependency carries potential CVEs.
The package manager alone—apt and its ecosystem—represents attack surface. An attacker who gains code execution inside your container can install additional tools, modify configurations, or pivot within the filesystem. The bash shell means they have an interactive environment for reconnaissance and lateral movement.
1Standard Image<br/>200+ MB2Distroless Image<br/>20-50 MBStandard images include man pages (/usr/share/man/), locale files (/usr/share/locale/), and documentation directories that serve no purpose in production. They contain shared libraries used by common utilities but not by your specific application. They include redundant copies of libc, openssl, and other foundational libraries because the image builder prioritized completeness over minimalism.
A single standard image can contain 400+ packages when you count every library and utility as a separate entity. Each package is a potential source of vulnerability.
What Distroless Images Strip Away
Distroless images take a radical approach: include only what your application needs to run, nothing more.
The Google Distroless project popularized this approach. A distroless/base image is roughly 19 MB compressed. What is actually missing? There are no package managers like apt, yum, or apk, so you cannot install anything after the image is built. No shell is provided—no /bin/bash, no /bin/sh, no dash—so interactive shell access is impossible. There are no utilities like curl, wget, grep, sed, awk, or find for text processing and data transfer. Man pages and documentation in /usr/share/ are stripped or minimal. No build tools like gcc, make, or development headers are included.
What remains is an absolute minimum: a few shared libraries (libc, libssl), certificate bundles for TLS validation, timezone data, and that is essentially it. Some distroless images are built directly on scratch—a completely empty base—with only statically-compiled binaries and the absolute runtime dependencies.
The trade-off is immediate: you cannot docker exec into a distroless container and run apt-get install to debug. You cannot log in with a shell. You cannot use standard debugging tools. What you gain is a dramatically reduced attack surface. A typical distroless image contains 15-30 packages instead of 400. The CVE density is orders of magnitude lower. A vulnerability in a utility you do not use cannot be exploited because the utility does not exist.
Attack Surface Comparison: Real Numbers
Consider a vulnerability scanner run against both image types. A standard ubuntu:22.04 baseline scan from October 2024 shows total packages of 427, critical CVEs of 8-12 (varies with release date), high severity issues of 25-35, and medium severity of 50-80. An example is CVE-2023-4911 (glibc buffer overflow) which affects all standard images.
A distroless python3.11-debian12 scan shows total packages of 22, critical CVEs of 0-2 (typically unpatched base OS kernels, not app-relevant), high severity of 2-4, and medium severity of 5-10.
The difference is not marginal; it is categorical. A standard image ships with vulnerabilities that were patched years ago but remain in the image because the system utilities depend on old library versions. A distroless image removes those utilities, and therefore those dependencies.
Real-world example: CVE-2019-3822 (curl buffer overflow). A standard image includes curl, making it vulnerable. A distroless image has no curl, making the CVE irrelevant regardless of whether a patch is available.
The Debugging Dilemma: How to Troubleshoot Without a Shell
The most cited objection to distroless images is the debugging problem: what do you do when something goes wrong in production?
The traditional debugging workflow would be:
$ docker exec -it container-id /bin/bash# ls -la /var/log/app/# tail -f /var/log/app/error.log# ps aux# netstat -tulnThis workflow is impossible with distroless. You cannot exec into the container and run commands. The shell does not exist.
This is not a flaw; it is a feature. The inability to exec and modify the container is a security property. But it requires a paradigm shift in how you approach debugging.
Logs must be observable before failure. Your application must emit structured logs to stdout/stderr. These logs flow to your centralized logging system (ELK, Splunk, DataDog, GCP Cloud Logging). Debugging happens by querying logs, not by shelling into the container.
Metrics must be exposed in real-time. Prometheus metrics, application performance monitoring, and request tracing (OpenTelemetry) allow you to understand behavior without terminal access. When a container misbehaves, metrics and traces show you why.
Health checks must catch problems early. Kubernetes liveness and readiness probes detect failures automatically. The container is restarted before a human needs to debug.
Process strace is not an option. You cannot run strace -p PID to see what syscalls a process is making. This constraint forces you to rely on application-level observability.
Workaround: Debug and Dev Image Variants
Many projects publish multiple image variants: myapp:1.0-distroless for production (minimal, no shell), myapp:1.0-debug for troubleshooting (contains busybox or a lightweight shell), and myapp:1.0 as the standard variant with full utilities.
In Kubernetes, you can temporarily deploy the debug variant into a staging environment, investigate, and then redeploy the production distroless variant.
Google's distroless project publishes explicit debug variants of their images. These are identical to the distroless base except they include /busybox—a single-binary Unix toolkit that provides basic shell functionality.
Example:
FROM distroless/python3-debian12:debug COPY myapp.py /app/WORKDIR /appENTRYPOINT ["python3", "myapp.py"]The debug variant weighs 40 MB instead of 25 MB, but it allows exec and basic troubleshooting.
Ephemeral Debug Containers: The Kubernetes Solution
Kubernetes 1.23+ introduced ephemeral debug containers, a mechanism to spawn a temporary debugging container that shares namespaces with a target pod.
kubectl debug -it pod-name --image=debian:latestThis command launches a Debian container that shares the network and IPC namespaces of the target pod. You get a shell, shell tools, and the ability to inspect the pod's network, processes, and IPC without modifying the production container image.
The target pod's distroless container continues running unchanged. The debug container is ephemeral—it terminates when your session ends. No artifacts are left behind.
This approach solves the debugging problem without compromising security: you keep production distroless images secure, but retain the ability to debug when needed.
The Spectrum: From Full OS to Scratch
Container images exist along a spectrum of minimalism. Tier 1, full operating systems, includes examples like ubuntu:22.04, centos:8, and debian:bookworm with size of 200+ MB unpacked. These work best for legacy applications, development environments, and systems that need every utility, but carry maximum CVE surface and maximum attack opportunities.
Tier 2, slim/minimal variants, includes examples like ubuntu:22.04-minimal, node:20-slim, and python:3.11-slim with size of 50-150 MB unpacked. These work for applications that need some standard utilities but not the full OS, with reduced but still significant CVE surface.
Tier 3, Alpine Linux, includes examples like alpine:3.19, python:3.11-alpine, and node:20-alpine with size of 5-30 MB unpacked. These work best for stateless applications, microservices, and environments where every MB matters, with minimal but non-zero CVE surface. Alpine uses musl instead of glibc, which can cause compatibility issues.
Tier 4, distroless, includes examples like distroless/base, distroless/python3, and distroless/nodejs20 with size of 15-50 MB unpacked. These work best for production microservices, security-sensitive applications, and cloud-native workloads, with lowest CVE surface but cannot exec or modify after build. Requires robust logging and observability.
Tier 5, scratch, includes statically-compiled Go binaries and fully self-contained applications with size of 1-10 MB unpacked. These work best for minimal microservices and single-binary applications with almost no CVE surface. Only viable for languages like Go or Rust that support static compilation.
Size Comparison Table
Image Type | Compressed | Unpacked | Packages | CVEs (avg) |
|---|---|---|---|---|
| 77 MB | 200 MB | 427 | 28 |
| 312 MB | 850 MB | 600+ | 40 |
| 125 MB | 330 MB | 180 | 18 |
| 48 MB | 140 MB | 85 | 6 |
| 68 MB | 180 MB | 22 | 2 |
Go binary (static) | 8 MB | 8 MB | 0 | 0 |
CVE counts are approximate and vary based on scan date and specific image version.
When to Use Standard Images
Standard images are not wrong. They are the right choice in specific contexts. When building locally or in a sandbox for development environments, you need tools. A standard image gives you apt, shell, editors, and everything you expect from a development machine. When debugging intermediate layers in a multi-stage build, you can use standard images in early stages and distroless in the final stage. If you need to troubleshoot the build, intermediate standard images provide tools. For legacy applications written for full Linux systems that often expect system utilities, refactoring them to work with distroless requires significant effort, so standard images are pragmatic for legacy code. For sidecar containers that perform housekeeping such as log rotation, config reload, and health checks, they may benefit from standard images because they need system-level utilities. Some runtimes (certain Java versions, some Python scientific stacks) have complex dependencies that distroless does not provide, making standard images necessary.
When to Use Distroless Images
Distroless is the right choice for production, particularly for cloud-native microservices. Stateless, containerized APIs and services running in Kubernetes are the ideal distroless use case, benefiting from minimal attack surface and easy debugging via logs and metrics.
For security-sensitive applications handling sensitive data, authentication, or financial transactions, minimize attack surface. Distroless dramatically reduces what an attacker can do if they gain code execution.
In compliance-heavy environments, many compliance frameworks (SOC2, PCI-DSS, HIPAA) require minimizing CVE surface. Distroless helps meet these requirements.
For supply chain security, when you care deeply about the provenance and security of every package, distroless removes hundreds of dependencies you did not explicitly choose.
For multi-tenant or untrusted code, if you run containers from untrusted sources, distroless limits what they can do. A shell-less environment is harder to exploit.
The Trade-Off Matrix
Dimension | Standard | Distroless |
|---|---|---|
Size | Large | Small |
Attack Surface | Large | Minimal |
CVEs Shipped | Many | Few |
Build Complexity | Low | Medium (need static binaries or specific runtimes) |
Debugging | Easy (exec into container) | Hard (need logs/metrics/traces) |
Observability Required | Low | High |
Image Pull Speed | Slower | Faster |
Multi-stage Build Needed | No | Often yes |
Operations Familiar | High | Lower (new paradigm) |
Compliance Friendliness | Moderate | High |
Migration Strategy: From Standard to Distroless
If you are running standard images in production and want to shift to distroless, the path is clear. First, audit your application's actual dependencies by running ldd against your binary to see what it actually links. You may find it needs far fewer libraries than you think.
Second, invest in observability first. Before switching to distroless, ensure you have structured logging, metrics, and distributed tracing. You are trading interactive debugging for observability-driven debugging.
Third, test locally with distroless. Build a distroless image and run it in development. Verify your app starts and behaves identically.
Fourth, deploy to staging with distroless. Run it in a non-production environment for days. Monitor for any unexpected behaviors.
Fifth, start with low-traffic services. Migrate your least critical, most stable services first. Gain confidence.
Sixth, use debug variants for incident response. Keep a debug variant available. If production fails mysteriously, you can spin up a debug variant to investigate.
Seventh, automate the build pipeline. Use multi-stage Dockerfiles that target distroless. Make it the default for your organization.
Real-World Examples
Node.js microservice:
# Stage 1: BuildFROM node:20 AS builderWORKDIR /appCOPY package*.json ./RUN npm ci --only=production # Stage 2: Runtime (distroless)FROM distroless/nodejs20-debian12WORKDIR /appCOPY --from=builder /app/node_modules ./node_modulesCOPY --from=builder /app/package.json ./COPY src ./srcENTRYPOINT ["node", "src/server.js"]This Dockerfile builds dependencies in a standard Node image (which has npm and build tools) but runs the application in distroless (which has only Node runtime). Final image: approximately 120 MB compressed.
Python Flask application:
FROM python:3.11-slim AS builderWORKDIR /appCOPY requirements.txt .RUN pip install -r requirements.txt FROM distroless/python3-debian12COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packagesCOPY app.py /app/WORKDIR /appENTRYPOINT ["python3", "app.py"]Runtime image: approximately 70 MB compressed, with only Python and your application code.
The Practical Reality
Standard and distroless are not either/or. The practical reality is a hybrid approach. Use distroless in production for maximum security. Use standard or slim images in development for tooling. Keep debug variants available for incident response. Use ephemeral debug containers for Kubernetes troubleshooting. Invest in structured logging and metrics to replace shell access.
The security-conscious approach is to start with distroless as the baseline and justify any deviation. Not the other way around.
Next Steps: Minimize Your Images Strategically
Understand the landscape — Know your options. Read Container Image Fundamentals to understand how images are structured and stored. Study Container Image Layers: Deep Dive to learn layer composition and attack surface. Explore Strip-Down vs Source-Built for comparison of minimization approaches.
Build minimalist images — Apply distroless principles. Read Build Stage Security to use multi-stage builds to minimize images. Study Pre-Build Stage Security to control dependencies before they enter your image. Learn Dockerfile to YAML Migration for declarative minimal specs.
Debug and operate safely — Address distroless constraints. Read Shell-Less Operations Guide to operate without shell access. Study Read-Only Filesystem Guide to make images immutable. Explore No-Shell, Read-Only Deep-Dive for architecture for minimal images.
Deploy and monitor — Keep minimal images secure. Read End-to-End Secure Deployment to deploy distroless safely. Study Helm Charts & Kubernetes to operate distroless at scale. Learn Container Registries Compared to store images efficiently.
