✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.1.1.1 Minimal Base Practice

A focused guide to Minimal Base Practice, connecting core concepts with practical Docker and container operations.

The minimal base practice, choosing the smallest base image that genuinely supports an application's needs, delivers a security benefit distinct from and arguably more important than the size reduction it also happens to provide: every package, shell, and utility present in a base image is something an attacker who gains code execution inside a compromised container can potentially use, which means a minimal base directly reduces what that attacker actually has available to work with.

Attack surface as a direct, countable property

Every installed package in a base image is a potential source of its own vulnerabilities, and every utility present is a potential tool for an attacker who has already achieved some level of code execution inside the container; a minimal base image directly reduces both of these countable, specific exposures:

docker run --rm node:20 dpkg -l | wc -l
docker run --rm node:20-alpine apk info | wc -l
docker run --rm gcr.io/distroless/nodejs20-debian12 sh
exec: "sh": executable file not found in $PATH

The package count difference between a full and minimal base image is often an order of magnitude, and a distroless image's complete absence of a shell at all represents the extreme end of this principle: there is no sh, no bash, nothing for an attacker to invoke interactively even with full code execution within the container, since the image was deliberately built without any general-purpose command interpreter at all.

Why no shell matters specifically

A shell is one of the most useful tools available to an attacker who has compromised a process, since it provides an interactive, general-purpose way to explore the filesystem, attempt further exploitation, or establish persistence; removing it entirely does not prevent every possible post-compromise action, but it meaningfully raises the difficulty and removes the most convenient, general-purpose tool an attacker would otherwise have immediately available:

docker exec -it compromised-container sh
OCI runtime exec failed: exec: "sh": executable file not found in $PATH

This same restriction applies equally to a legitimate operator trying to debug the container interactively, which is the genuine trade-off minimal and distroless images impose, and is worth weighing deliberately rather than adopted reflexively for every service regardless of its actual risk profile and operational needs.

No package manager means no easy tool installation

A minimal base image's absence of a package manager means an attacker cannot simply install additional tools after compromising the container, the way they could against a full-featured base image with apt or apk readily available:

docker exec compromised-container apt-get install -y netcat
exec: "apt-get": executable file not found in $PATH

This forces an attacker to bring along whatever tools they need rather than relying on what is conveniently already installed or trivially obtainable through the compromised container's own package manager, which is a meaningful, if not absolute, additional barrier.

Defense in depth, not a single complete solution

It is important to be precise about what minimal base images actually provide: they reduce the attack surface and raise the difficulty of certain post-compromise actions, but they do not prevent the initial compromise itself, nor do they address vulnerabilities in the application's own code or its legitimate runtime dependencies, which remain present regardless of how minimal the surrounding base image is:

docker scout cves my-api:1.4.2

A minimal base practice is one layer within a broader defense-in-depth strategy that should also include application-level security review, dependency vulnerability scanning, network segmentation, and least-privilege user configuration, rather than being treated as a complete security solution on its own.

Comparing exposure across the base image spectrum

The reduction in attack surface scales fairly directly with how minimal the base image actually is, which provides a useful, concrete way to reason about the trade-off when choosing among several viable options:

Full distribution (e.g. debian, ubuntu)  → largest surface, full shell, package manager, many utilities
Slim variant                              → reduced surface, shell retained, fewer pre-installed utilities
Alpine                                    → considerably reduced surface, minimal shell (ash/BusyBox), package manager present
Distroless                                → no shell, no package manager, runtime libraries only
Scratch (with static binary)              → no operating system at all beyond the application binary itself

Each step down this spectrum trades operational convenience and debugging capability for a more constrained, harder-to-exploit environment, and the appropriate point on this spectrum depends on the specific service's actual risk profile, exposure (public-facing versus purely internal), and operational debugging needs.

When minimal is not the right trade-off

For services genuinely requiring frequent interactive debugging, or those depending on tooling that is difficult or impractical to statically include without a fuller base image's package ecosystem, a less extreme point on the spectrum, Alpine rather than distroless or scratch, may be the more practical, appropriate choice despite the somewhat larger attack surface it retains:

FROM node:20-alpine

This remains a meaningful security improvement over a full, unminimized base image while preserving enough basic tooling (a shell, a package manager for emergency, deliberate troubleshooting installs) to remain practically debuggable during an actual incident.

Verifying the minimal base still meets the application's actual needs

Before committing to a more minimal base image purely for its security benefit, confirming the application actually runs correctly against it, particularly for compiled dependencies sensitive to the glibc-versus-musl distinction relevant to Alpine specifically, avoids trading a security improvement for a functional regression that then requires reverting the change anyway:

docker build -t my-api:alpine-test --build-arg BASE=node:20-alpine .
docker run --rm my-api:alpine-test npm test

Common mistakes

  • Treating minimal base image adoption as a complete security solution rather than one layer within a broader defense-in-depth strategy.
  • Choosing the most extreme minimal option, such as scratch, for every service regardless of actual debugging needs or risk profile, without weighing the genuine operational trade-off.
  • Not verifying an application actually runs correctly against a more minimal base before committing to it, particularly for native dependencies with compatibility concerns.
  • Assuming the absence of a shell or package manager prevents all forms of post-compromise activity, rather than understanding it as raising difficulty and removing convenient tooling specifically.
  • Overlooking that application code and legitimate dependencies remain a security concern regardless of how minimal the surrounding base image is, requiring separate, ongoing attention through scanning and code review.

The minimal base practice delivers a genuine, specific security benefit by directly reducing what an attacker has available after a compromise, no shell, no package manager, far fewer installed packages and their associated vulnerabilities, but it should be understood and applied as one component of a layered security approach, with the specific point on the minimalism spectrum chosen deliberately based on each service's actual risk profile and operational needs rather than applied uniformly without that consideration.