Skip to main content

What are distroless images and what advantages do they offer?

Distroless images are stripped-down container images. Google's GoogleContainerTools team coined the term: "distribution-less" — no full Linux distribution, just the bare minimum to run an app. The result is dramatically smaller and more secure than traditional images.

Theory

TL;DR

  • A distroless image contains: the language runtime + your binary + CA certs. That is it.
  • No shell (no bash, no sh).
  • No package manager (no apt, no apk).
  • No utilities (no curl, no vim, no ps).
  • Maintained by Google: gcr.io/distroless/<runtime>.
  • Variants: static, base, cc, python3, nodejs22, java21, etc. Plus :debug and :nonroot flavors.
  • Why use it: smaller image (often 5-50 MB final), drastically smaller attack surface, faster CVE remediation (less stuff to scan).

Distroless variants

ImageContentsUse for
gcr.io/distroless/staticnothing but glibc + CA certsStatic binaries (Go, Rust)
gcr.io/distroless/basestatic + glibc + busybox-staticMostly static binaries with minor deps
gcr.io/distroless/ccbase + libgccC/C++ binaries with compiler runtime
gcr.io/distroless/python3Python runtimePython apps
gcr.io/distroless/nodejs22Node 22 runtimeNode apps
gcr.io/distroless/java21Java 21 runtimeJVM apps

Each has :debug and :nonroot (preferred) variants.

Quick example: Go static binary

dockerfile
FROM golang:1.23-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server ./cmd/server FROM gcr.io/distroless/static:nonroot COPY --from=build /out/server /server ENTRYPOINT ["/server"]

Final image: ~10 MB. Just the Go binary + the bare runtime support files. No shell to drop into, no apt-get, nothing that does not directly serve the app.

Node.js example

dockerfile
FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . RUN npm run build FROM gcr.io/distroless/nodejs22:nonroot WORKDIR /app COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules CMD ["dist/server.js"]

Final image: ~150 MB (Node runtime is large). Compare to node:22 (~1 GB) or even node:22-alpine (~200 MB) — and the alpine has shell and apk.

Why distroless

Security

  • No shell = no shell-based exploits. A compromised app cannot exec bash.
  • No package manager = attacker cannot install tools to escalate.
  • Fewer files = fewer CVEs. Vulnerability scanners report fewer findings; remediation is just "bump base image".

Size

  • 80-95% smaller than full Debian/Ubuntu.
  • Smaller pulls, faster cold starts, less storage.
  • Cumulative impact at scale: hundreds of services × millions of pulls = real bandwidth savings.

Predictability

  • The image contains exactly what you put in. No surprise utilities. No drift between dev and prod.

The debugging trade-off

The biggest objection to distroless: how do I docker exec sh?

Answer: you do not. The trade-off is real. Three workarounds:

1. :debug variant

bash
docker run -it --entrypoint sh gcr.io/distroless/base:debug

The :debug tag adds busybox. Use this for development; deploy :nonroot to production.

2. Sidecar debug container

In Kubernetes:

bash
kubectl debug -it pod/myapp --image=alpine --target=app

Runs an alpine shell that shares namespaces with your distroless container. You can poke at the app's filesystem and processes from a sister container.

3. Build separate :debug image

dockerfile
FROM gcr.io/distroless/nodejs22:debug AS debug COPY --from=build /app /app

Produce two tags: myapp:1.0 (distroless) and myapp:1.0-debug (with shell). Deploy the debug one when needed.

Comparison with Alpine

AlpineDistroless
Has shell?Yes (busybox sh)No
Has package manager?Yes (apk)No
Size of base~7 MB2-50 MB depending on variant
Easy to debug?YesNo (need :debug variant)
Multi-stage friendly?YesYes
Glibc or musl?muslglibc
CVEs reportedSome (APK packages)Very few

Alpine is the size champion among full distros; distroless beats it on attack surface but at the cost of debuggability. Many projects use alpine as builder + distroless as final.

Common mistakes

Trying to docker exec sh on a distroless image

bash
$ docker exec -it myapp sh OCI runtime exec failed: exec failed: ... "sh": executable file not found in $PATH

The shell is not there. For debugging, use the :debug variant or the sidecar pattern.

Running healthchecks that need curl

dockerfile
FROM gcr.io/distroless/nodejs22:nonroot HEALTHCHECK CMD curl -f http://localhost:3000/health # Fails: no curl in distroless

Solutions:

  • For Node: HEALTHCHECK CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"
  • For others: include a tiny static health-checker binary; or use external healthcheck (Kubernetes probe instead of Docker healthcheck).

Using gcr.io/distroless/base for a Go binary that needs glibc

dockerfile
FROM gcr.io/distroless/static AS final # ← may not work for CGO-enabled binaries

If your Go binary uses CGO (any C interop), use cc or base variant. For pure Go, use static.

Forgetting :nonroot

dockerfile
FROM gcr.io/distroless/static # ← runs as root by default

Use gcr.io/distroless/static:nonroot (UID 65532) by default. Reduces blast radius if escape happens.

Real-world adoption

  • Google internal services: distroless is the default for production images (Google created it).
  • Kubernetes ecosystem: many CNCF projects ship distroless images (Prometheus components, kube-state-metrics).
  • Banking / regulated industries: distroless reduces auditor pain and CVE management overhead.
  • Serverless: smaller images = faster cold starts. AWS Lambda, Cloud Run benefit.

Follow-up questions

Q: Can I write a Dockerfile that starts FROM distroless directly (without multi-stage)?


A: Technically yes, but you cannot install anything (no shell, no apt). The distroless base is meant as the target of a multi-stage build, not the builder.

Q: What is the difference between static and base?


A: static has just glibc and CA certs. base adds busybox-static (a few utilities). Use static for Go/Rust pure-static binaries; base for C/C++ that need a few utilities.

Q: Are distroless images signed?


A: Yes — Google signs them via Cosign. Verify with cosign verify gcr.io/distroless/base --certificate-identity=....

Q: Is there a Wolfi or Chainguard equivalent?


A: Yes — Chainguard Images is a popular alternative that has been gaining ground. Wolfi-based, similar minimalism, often with even faster CVE patching. Worth evaluating for new projects.

Q: (Senior) When would you NOT use distroless?


A: Three cases. (1) Your app needs a shell at runtime (rare; usually a smell). (2) You need many tools at runtime that would require building a custom minimal image (better: rethink the app). (3) The team is not yet equipped to debug without docker exec sh — invest in tooling (sidecar debug, observability) before forcing distroless on them. For greenfield Go/Rust services, distroless is a no-brainer; for legacy apps with shell-dependent operations, migrate gradually.

Examples

Go in scratch (even smaller than distroless)

dockerfile
FROM golang:1.23-alpine AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /out/server ./cmd/server FROM scratch COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=build /out/server /server USER 65532:65532 ENTRYPOINT ["/server"]

scratch is even more minimal than distroless. Just the binary + CA certs. Final image: roughly the size of the binary.

Distroless Node with healthcheck

dockerfile
FROM node:22-alpine AS build WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . RUN npm run build FROM gcr.io/distroless/nodejs22:nonroot WORKDIR /app COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD ["node", "-e", "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"] CMD ["dist/server.js"]

No curl in distroless; we use the Node runtime as our healthcheck binary. Same effect, no extra tools needed.

Production-like Python

dockerfile
FROM python:3.13-slim AS build WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt COPY . . FROM gcr.io/distroless/python3:nonroot WORKDIR /app COPY --from=build /install /usr/local COPY --from=build /app /app USER nonroot:nonroot CMD ["app.py"]

Python runtime + your code + dependencies, nothing else. Final image around 50-150 MB depending on dep size — much less than python:3.13 slim's ~150-300 MB.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Comments

No comments yet