Distroless container images: shipping without a shell
Ship images with no package manager, no shell, and almost no attack surface — and still debug when you need to.
Most container break-ins follow a script: get code execution, spawn a shell, curl a second stage, enumerate with coreutils. Distroless images remove that script's props — your app, glibc or static runtime, and CA certs — but no sh, no package manager, and no wget. You trade convenience for an attack surface measured in megabytes instead of hundreds.
This walkthrough builds a Go service on distroless/static, shows why docker run … sh fails (on purpose), and covers debug tags without baking tools into prod. Pair it with multi-stage patterns from Docker in depth if you are still shipping compilers in the same stage as your binary.
Distroless is always the second stage. You cannot apk-install into it — everything the process needs must be copied in or embedded at build time.
What is gone, and why defenders care
A debian:bookworm-slim base still ships bash, dpkg, and dozens of packages you will never patch individually. Distroless flips the model: Google maintains minimal bases with regular rebuilds, and your scanner stops reporting CVEs in tools you never invoked. You still patch your app and any copied libs — but the long tail of OS utilities disappears.
docker run --rm -it gcr.io/distroless/static:nonroot shexec: "sh": executable file not found in $PATHthat failure is the feature — post-exploit tooling is missingdocker run --rm gcr.io/distroless/static:nonroot /appapp starts when ENTRYPOINT targets your binary directlyA production-shaped Dockerfile
Choose the variant that matches your linkage: static for pure Go, base for glibc dynamic binaries, java for JRE-only workloads. Tag :nonroot unless you have a rare reason to run as root — the default user is 65532:65532.
FROM golang:1.22 AS buildWORKDIR /srcCOPY . .RUN CGO_ENABLED=0 go build -trimpath -o /app ./cmd/serverFROM gcr.io/distroless/static:nonrootCOPY --from=build /app /appUSER nonroot:nonrootENTRYPOINT ["/app"]
Operational tradeoffs you must accept
Shell-form CMD curl -f http://localhost/health breaks — health checks must exec your binary or an HTTP probe built into the app. docker exec debugging is gone; use kubectl debug ephemeral containers, the :debug tag (busybox sidecar), or centralized logs. Distroless rewards teams that invest in observability instead of SSH-within-a-container.
Language-specific distroless tags matter: gcr.io/distroless/java17-debian12 ships a JRE without apt; nodejs20-debian12 exists for interpreted workloads where copying only /app is not enough. Read the image README for CA cert and timezone needs — static Go binaries often need nothing beyond the binary itself, while JDBC clients may need additional trust bundles copied explicitly.
Where this goes next
Distroless is the runtime half of a secure image story; multi-stage is the build half. Add admission policies that reject :latest and root UID 0, and study escape paths so you know what minimal images actually block. Advanced container security walks seccomp, capabilities, and breakout techniques hands-on in a lab cluster.