Suggest an editImprove this articleRefine the answer for “What are distroless images and what advantages do they offer?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Distroless images** contain only what your app actually needs to run — the language runtime and your binary. **No shell, no package manager, no apt, no busybox.** Result: small (often <50 MB), with near-zero attack surface. ```dockerfile 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"] ``` **Key:** smaller, more secure, faster pulls. Trade-off: no `docker exec sh` — debugging requires `:debug` variants. Best fit for production runtime images of compiled or interpreted apps.Shown above the full answer for quick recall.Answer (EN)Image**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 | Image | Contents | Use for | |---|---|---| | `gcr.io/distroless/static` | nothing but glibc + CA certs | Static binaries (Go, Rust) | | `gcr.io/distroless/base` | static + glibc + busybox-static | Mostly static binaries with minor deps | | `gcr.io/distroless/cc` | base + libgcc | C/C++ binaries with compiler runtime | | `gcr.io/distroless/python3` | Python runtime | Python apps | | `gcr.io/distroless/nodejs22` | Node 22 runtime | Node apps | | `gcr.io/distroless/java21` | Java 21 runtime | JVM 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 | | Alpine | Distroless | |---|---|---| | Has shell? | Yes (busybox sh) | No | | Has package manager? | Yes (apk) | No | | Size of base | ~7 MB | 2-50 MB depending on variant | | Easy to debug? | Yes | No (need :debug variant) | | Multi-stage friendly? | Yes | Yes | | Glibc or musl? | musl | glibc | | CVEs reported | Some (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.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.