GoogleContainerTools/distroless: language-focused container images without a Linux userland
🥑 Language focused docker images, minus the operating system.
At a glance
- What is it?
- Distroless images ship an application and its runtime dependencies and nothing else, so the container has no shell, no package manager and no coreutils. This review covers the published image list, how a multi-stage Docker build consumes them, and the cases where the missing shell becomes the wrong tool.
- Who is it for?
- Adopt distroless when your build already produces a self-contained artifact and your team can debug through logs and the debug tag rather than an interactive shell. Do not adopt it for images that install packages at runtime, expect bash, or need a package manager inside the container.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Starlark, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 29, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What distroless removes, and who the removal helps
A standard Linux base image gives you a distribution: a shell, a package manager, coreutils, and a long tail of libraries that your program never calls. Distroless inverts that. According to the README, these images "contain only your application and its runtime dependencies" and deliberately exclude package managers, shells and other programs expected in a standard distribution. The unit of composition is the language runtime, not the operating system.
The audience is narrow and identifiable. If you build a Go binary with CGO disabled, a compiled Rust executable, a Java jar, or a Node.js or Python application with a fixed dependency set, the runtime surface you actually need is small and known at build time. The README frames the payoff as scanner signal: fewer packages means fewer CVE findings that have nothing to do with your code, and provenance work shrinks to the artifacts you chose to include. Size follows from the same decision. The README states the smallest image, gcr.io/distroless/static-debian13, is around 2 MiB, roughly half of alpine at about 5 MiB and under two percent of debian at 124 MiB. Those are the project's own figures, not an independent measurement.
The trade is explicit: you give up the ability to enter the container and poke at it. That is the point, not a defect, but it changes how incidents get handled.
How the image families map to language runtimes
The project publishes image indexes with per-architecture tags, and architecture-specific images are addressable with a suffix such as gcr.io/distroless/static-debian13:latest-amd64. The README states that any tags outside the documented set are deprecated and no longer updated, which matters when you inherit a Dockerfile pinned to something old.
The Debian 13 families split by what the runtime needs. static-debian13 is the floor: no libc, no SSL. base-debian13 adds the base layer. base-nossl-debian13 drops the OpenSSL libraries for programs that bring their own TLS. cc-debian13 carries the C/C++ runtime for binaries that link against glibc. Above that sit language images: java-base, java17, java21 and java25, nodejs22, nodejs24 and nodejs26, and python3. Architecture coverage is not uniform. The java and python3 images list amd64, arm64 and a subset of others, while static, base, base-nossl and cc cover amd64, arm64, arm, s390x, ppc64le and riscv64. If you target an unusual architecture, check the row before you commit.
One Debian 13 detail has teeth. These images use the UsrMerge scheme, and the README notes that if you add packages with rules_distroless, you must set mergedusr = True in apt.install. Get that wrong and the added files land in paths the runtime does not search.
Building a Go image on static-debian13
Distroless images are built with bazel, but the README is clear that they can be consumed through other Docker image build tooling. The documented path is a multi-stage Dockerfile: one stage compiles, the second copies the artifact into a distroless base. The README's Go example uses a golang build stage and a static-debian13 runtime stage.
FROM golang:1.18 as build
WORKDIR /go/src/app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o /go/bin/app
FROM gcr.io/distroless/static-debian13
COPY --from=build /go/bin/app /
CMD ["/app"]The CGO_ENABLED=0 flag is what makes static-debian13 viable: the resulting binary does not need a C library, so the image without one is sufficient. The final stage copies exactly one file. There is no apt-get, no apk add, and nowhere for either to run.
To build and run it, the README gives two commands from the example directory.
docker build -t myapp .
docker run -t myappThe build should succeed without installing anything into the runtime stage, and the run should start the binary directly. The README also points to ready-made examples under examples/java, examples/python3, examples/go, examples/nodejs and examples/rust for the other runtimes. For the Node.js Express example in examples/nodejs/node-express, the documented sequence installs dependencies first, then builds, then publishes port 3000.
npm install
docker build -t myexpressapp .
docker run -p 3000:3000 -t myexpressappIf the container starts and immediately exits with no output, check the ENTRYPOINT form before anything else.
The ENTRYPOINT form is not optional
Because there is no shell in the image, the runtime cannot prefix a command with /bin/sh -c. The README states this directly: the Dockerfile ENTRYPOINT, when defined, must be in vector form. The working form is ENTRYPOINT ["myapp"]. The broken form is ENTRYPOINT "myapp".
The same rule applies to CMD when the entrypoint is the empty vector. The README notes that static, base and cc images default to an empty vector entrypoint, while images with a language runtime ship a language-specific default documented in the java, nodejs and python3 subdirectory READMEs. That default is a convenience and a trap: if you assume every distroless image starts your binary for you, the language images will disagree.
This is the single most common way a first distroless migration fails, and it fails quietly. The image builds, the registry accepts the push, and the container exits. Nothing in the build output warns you, because from Docker's perspective the Dockerfile is valid. Treat vector-form ENTRYPOINT and CMD as a lint rule, not a style preference.
Signature verification with cosign
Distroless images are signed with cosign using ephemeral keys, which the README describes as keyless. The project recommends verifying any distroless image before building on top of it. The documented command pins both the OIDC issuer and the signing identity.
cosign verify $IMAGE_NAME --certificate-oidc-issuer https://accounts.google.com --certificate-identity [email protected]Substitute the exact reference you intend to use, including the architecture suffix if you are not consuming the index. A successful verification confirms the image came from the distroless signing identity. It does not tell you whether the tag still points where it did last month, so pin by digest if your pipeline needs that guarantee.
There is an oddity worth naming. The images are served from the gcr.io domain even though the README says the serving infrastructure has moved to artifact registry. The stated reason is that users get the newer infrastructure without changing their builds. Convenient, but it means the hostname in your Dockerfile no longer describes where the bytes come from.
Where distroless is the wrong tool
The absence of a shell is the headline feature and also the main failure mode. Any container that needs to run a command at startup (a migration script invoked through sh, an entrypoint wrapper that templates a config file with sed, a healthcheck that calls curl) cannot do so here. You either move that work into the build stage, rewrite it in the application, or pick a different base.
The debug tag is the documented escape hatch. The image list includes debug and debug-nonroot variants alongside latest and nonroot for every family, and those are the ones to reach for when you need to inspect a running container. They are also larger, so shipping them to production defeats part of the purpose. A team that cannot debug without a shell will end up deploying debug images permanently, and the size and scanner advantages disappear.
A second boundary is dynamic dependency loading. If your application resolves plugins, drivers or native libraries at runtime from the filesystem, distroless gives you nothing to resolve them against. The README does not document a supported path for installing packages into a published image; the rules_distroless reference is about building your own, which is a different workflow from pulling gcr.io/distroless/*.
Finally, if your team's operational model depends on kubectl exec into a container to read files or restart a process, distroless removes that entirely. That is a real cost, paid in incident response time, and it should be weighed before the migration rather than after.
Distroless compared with alpine and with scratch
The two comparisons people reach for are alpine and scratch, and they fail in different directions.
Alpine is a full distribution in miniature. It has a shell, apk, busybox and musl libc. The README's size comparison puts alpine at roughly 5 MiB against static-debian13 at about 2 MiB, but the more consequential difference is not the three megabytes. It is that alpine's musl libc and its package manager create a runtime surface that needs patching. You inherit a CVE feed for components your application never calls. Distroless removes that surface rather than shrinking it. The cost is that you also remove the ability to install a debugging tool on the spot.
Scratch is the opposite extreme: an empty filesystem with nothing at all. It is smaller than any distroless image because it contains literally nothing. What it lacks is the curated middle ground. Distroless static-debian13 ships the certificates, timezone data and passwd entries that a program expects to find, and the base and cc families add the C library and OpenSSL that compiled binaries link against. With scratch you assemble all of that yourself, and the assembly is where mistakes live. Distroless is the opinionated version of that assembly, published and signed.
The choice is therefore about who owns the file list. Alpine hands you too much, scratch hands you nothing, and distroless hands you a specific set that the project maintains per language and architecture.
Maintenance, licensing and what to check before adopting
The repository is not archived, and the last push was on 2026-09-19. That is a recent commit, and the CI badge in the README points at a workflow that runs on every change. The README also refers to SUPPORT_POLICY.md for support timelines, which is where the actual end-of-life dates for each Debian generation live; this review did not read that file, so treat the tag list as the current state and the policy file as the authority on how long it holds.
The licence is Apache-2.0, which covers the repository contents. That is separate from the licensing of the Debian packages and language runtimes bundled inside each image, and separate again from your own application's licence. Pulling a distroless base does not change what your binary is licensed under, but redistributing an image does carry the obligations of everything inside it. That is a question for your legal team, not for this article.
The upgrade cost is the Debian generation. The published families are all Debian 13 today, and when Debian 14 arrives the image names change with it. Because the tags embed the generation (static-debian13, base-debian13, and so on), an upgrade is a Dockerfile edit, not a floating tag that moves underneath you. That is a good property. It also means nothing upgrades automatically, and a pinned old generation keeps receiving nothing once it leaves the support window.
What to verify first: that your ENTRYPOINT and CMD are in vector form, that every architecture you deploy is listed for the image family you chose, and that cosign verify passes for the exact reference you will pin.
Editorial conclusion
Adopt distroless when your build already produces a self-contained artifact and your team can debug through logs and the debug tag rather than an interactive shell. Do not adopt it for images that install packages at runtime, expect bash, or need a package manager inside the container. Before switching, verify that your Dockerfile uses vector-form ENTRYPOINT and CMD, and run cosign verify against the exact image reference you intend to pin.
Frequently asked questions
What does distroless mean for a container image?
It means the image contains your application and its runtime dependencies but not a Linux distribution. The README states these images do not contain package managers, shells or other programs you would expect to find in a standard distribution.
Why use distroless images instead of a full base image?
The README argues that restricting the runtime container to what the app needs improves the signal to noise of CVE scanners and reduces the burden of establishing provenance. The size difference is a side effect of the same decision.
How do I use a distroless image in a Dockerfile?
The documented approach is a multi-stage build: compile in a full language image, then copy the artifact into a distroless runtime stage. The README's Go example builds with CGO_ENABLED=0 and copies the binary into gcr.io/distroless/static-debian13, with CMD in vector form.
What is gcr.io/distroless/static?
It is the smallest published distroless family, available as gcr.io/distroless/static-debian13. The README gives its size as around 2 MiB and notes that static, base and cc images default to an empty vector entrypoint, so CMD must be specified in vector form.
Which distroless images are available?
The README lists Debian 13 families for static, base, base-nossl, cc, java-base, java17, java21, java25, nodejs22, nodejs24, nodejs26 and python3, each with latest, nonroot, debug and debug-nonroot tags. Architecture suffixes such as -amd64 are used to reference a specific architecture directly.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/googlecontainertools-distroless)