# distroless images: when to use them

## TL;DR

Distroless images contain your app and its runtime dependencies and almost nothing else: no shell, no package manager, no utilities. That means smaller images, far fewer CVEs for scanners to complain about, and less for an attacker to use if they get in. Use them for compiled or self-contained apps via multi-stage builds. Skip them when your app needs a shell at runtime or you debug by exec-ing in constantly.

```text
distroless images: when to use them
```

## Use this when

- Your image scanner reports CVEs in OS packages your app never touches
- You want smaller images for faster pulls and deploys
- You are hardening containers and want to remove the shell as an attack tool
- You are choosing between distroless, alpine, and slim base images

## Not for this skill when

- You want general Dockerfile optimization (layer caching, build speed)
- Your app shells out to system utilities at runtime
- You need an interactive debugging workflow inside the container

## Steps

### 1. See what is (not) inside

```bash
docker run --rm gcr.io/distroless/static-debian13:latest whoami
```

Expected: an error like "executable file not found in PATH". There is no whoami, no sh, no ls. That is the whole point: if it is not there, it cannot be exploited and cannot show up in a CVE scan.

### 2. Pick the right variant

Static is for statically compiled binaries (Go with CGO disabled, Rust). Base adds glibc, ca-certificates, and timezone data. There are also cc, java, python, and nodejs variants, plus nonroot variants that run as an unprivileged user. Match the variant to what your app actually needs at runtime, nothing more.

### 3. Build with a multi-stage Dockerfile

Compile in a full builder image, copy only the artifact into distroless. Save this as Dockerfile:

```dockerfile
FROM golang:1.24 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

FROM gcr.io/distroless/static-debian13
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]
```

```bash
docker build -t myapp:distroless .
```

Expected: the build succeeds and the resulting image is tens of megabytes instead of hundreds. The Go toolchain and source never ship in the final image.

### 4. Debug without a shell

You cannot kubectl exec into a distroless container and poke around. Attach an ephemeral debug container that shares the target's namespaces instead:

```bash
kubectl debug -it [pod-name] --image=busybox --target=[container-name]
```

Expected: a shell prompt in the debug container, sharing the target's process namespace and filesystem view. You get your tools without baking them into the production image.

### 5. Confirm the CVE noise actually dropped

```bash
trivy image myapp:distroless
```

Expected: dramatically fewer OS-package findings than the same app on a full base image. Remaining findings should be about your app's own dependencies, which is the signal you actually care about.

### Variant: distroless vs alpine

Alpine is small and has a shell and package manager, which is convenient and also exactly what scanners and attackers use. Pick alpine when you need shell access or musl works for you. Pick distroless when you want the smallest attack surface and do not need a shell at runtime.

### Variant: missing certs or timezone data

The static variant has no CA certificates or timezone database, which breaks TLS and time formatting. Switch to the base variant, or copy the files from the builder stage. Do not "fix" it by installing packages at runtime.

### Variant: chainguard images as an alternative

Chainguard's images follow the same minimal philosophy with daily rebuilds and SBOMs. Same decision logic applies: great for hardening, same debugging tradeoff.

## Why this happens

Most CVEs in container scans come from OS packages the app never uses: shells, text utilities, package managers dragged in by a full Debian or Ubuntu base. Attackers likewise love landing in a container with a shell and curl. Distroless removes the unused 95 percent, so scanners go quiet and post-exploitation gets much harder.

## Edge cases and pitfalls

- Healthchecks must be TCP or HTTP based. A shell-form healthcheck has no shell to run in.
- ENTRYPOINT must use the exec form. There is no shell to interpret shell form.
- Apps that shell out at runtime (calling ffmpeg, git, or similar) need those binaries present. Distroless is the wrong base for them.
- The nonroot variants run as an unprivileged user. Make sure file ownership in the image matches.
- Language-specific variants lag behind new runtime releases sometimes. Check the tags.
- Debugging with ephemeral containers needs a cluster version that supports kubectl debug.

Tool notes: distroless images are published for debian11, debian12, and debian13 variants. Multi-stage builds need Docker 17.05+ or any BuildKit setup.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_BqEZeaEAf8u-ZA6VlAIDWg
