Distroless Container Images

DevOpsDockerSecurityContainers
Share on LinkedIn

A production container that ships with apt, curl, and an interactive shell is a production container with a built-in pivot point for attackers. Distroless images strip the operating system down to your application and its runtime dependencies — nothing else. No package manager, no shell, no stray utilities that CVE scanners love to flag. The trade-off is real: you cannot docker exec into a shell and poke around. But that inconvenience is exactly why teams at Google and elsewhere use distroless as the final stage in multi-stage builds.

What distroless actually contains

Google's distroless images are not "Alpine but smaller." They are curated runtime bases:

Each image runs as a non-root user by default in recent tags. You copy your compiled artifact in from a builder stage; the runtime image never saw apt-get install.

# Build stage — full toolchain
FROM golang:1.23-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/server

# Runtime — distroless static, non-root
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

The resulting image is often 10–20 MB instead of 300+ MB for a full Debian base, and docker scout or Trivy reports far fewer packages to patch.

Multi-stage patterns that work

Distroless shines in multi-stage Dockerfiles. The builder stage does heavy lifting; the final stage receives only binaries and required assets.

For Node.js, compile native addons in a node:20-bookworm builder, then copy node_modules and application code into gcr.io/distroless/nodejs20-debian12. For Java, use a Maven or Gradle stage and copy the fat JAR into distroless/java17-debian12. For Python, install wheels in a builder with pip install --target=/deps, then copy into distroless/python3-debian12.

One mistake I see repeatedly: copying the entire project directory including .git, tests, and dev configs. Copy only what the runtime needs. Use .dockerignore aggressively.

Health checks without curl

Distroless has no curl or wget. Options:

  1. Orchestrator probes — Kubernetes HTTP/TCP probes hit your app's port directly; no in-container probe binary needed.
  2. Application-native health endpoint — your app listens on /healthz; the kubelet probes it externally.
  3. GRPC health — if you use gRPC, implement the standard health service.

Avoid installing curl via a hacky COPY --from=busybox. That defeats the purpose. If you truly need an in-container probe binary, consider whether your health check belongs outside the container.

Debugging production issues

When a distroless pod crash-loops, your workflow changes:

# Inspect logs — still works
kubectl logs pod/my-app-abc123 --previous

# Temporary debug deployment — same tag, debug variant
# gcr.io/distroless/base-debian12:debug includes busybox
kubectl debug pod/my-app-abc123 -it --image=gcr.io/distroless/base-debian12:debug --target=my-app

Locally, reproduce with docker run --rm -it gcr.io/distroless/base-debian12:debug and mount your binary. Keep a runbook that maps each production image to its debug counterpart. Restrict debug image pulls in CI/CD so they never replace production tags accidentally.

Security and compliance wins

Fewer packages mean fewer CVEs in scan reports and less noise for your security team. Distroless images are rebuilt regularly when Debian security updates land. Pin by digest in production manifests (image: gcr.io/distroless/static-debian12@sha256:...) rather than floating :latest.

Distroless does not replace application security. Your app can still misconfigure TLS, log secrets, or expose admin endpoints. It removes the OS layer as an easy lateral movement path — an attacker who exploits your app cannot spawn a shell or download tools from the compromised container without additional exploits.

When not to use distroless

Skip distroless for local development images, CI jobs that need shell scripting inside the container, or legacy apps that expect to shell out to system utilities at runtime. Use full bases for those stages; only the deployed artifact stage should be distroless.

Treat production rollout as a measured change: ship with observability, validate rollback, and review metrics 24 hours after deploy — patterns that look obvious in docs fail when skipped under release pressure.

Common production mistakes

Teams get distroless images wrong in predictable ways:

Production implementations of distroless images fail when staging mirrors production topology poorly, rollback is untested, and on-call runbooks describe the happy path only.

Debugging and triage workflow

When distroless images misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

Can I run shell commands inside a distroless container?

No — distroless images omit /bin/sh, package managers, and most standard utilities by design. That is the security benefit. Use multi-stage builds to copy artifacts into distroless, run debug variants (distroless/debug) temporarily, or attach ephemeral debug sidecars in Kubernetes rather than baking a shell into production.

Which distroless variant should I use for a Go binary?

Use gcr.io/distroless/static-debian12 or distroless/static:nonroot for statically linked Go binaries. If your binary needs CA certificates or timezone data, use distroless/base instead. Match the Debian version (debian11 vs debian12) to your build stage libc expectations.

How do I troubleshoot a crashing distroless container?

Check exit codes and logs from the orchestrator first. For deeper inspection, deploy the same image tag with the distroless/debug variant, which includes busybox shell, or copy the binary into a debug stage locally. Never leave debug images in production registries without strict access controls.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →