Distroless container images
Three families of base images published on GHCR: a trimmed Java runtime, an nginx build with a WAF compiled in, and builder/runtime pairs for Node.js. They all sit on Chainguard Wolfi, run as a non-root user with no shell and no package manager, and rebuild nightly so the base never drifts behind its security patches.
The images
| Image | Base | Contents | Pull | Platforms |
|---|---|---|---|---|
| jre-distroless-25 | Wolfi glibc-dynamic | jlink-trimmed Temurin 25 LTS | 46 MB | amd64, arm64 |
| jre-distroless-21 | Wolfi glibc-dynamic | jlink-trimmed Temurin 21 LTS | 44 MB | amd64, arm64 |
| jre-distroless-17 | Wolfi glibc-dynamic | jlink-trimmed Temurin 17 LTS | 40 MB | amd64, arm64 |
| nginx-distroless | Chainguard static | static nginx + ModSecurity v3 + OWASP CRS | 7 MB | amd64 |
| nodejs-distroless-24-runtime | Wolfi glibc-dynamic | node 24 binary + shared libs | 41 MB | amd64 |
| nodejs-distroless-22-runtime | Wolfi glibc-dynamic | node 22 binary + shared libs | 40 MB | amd64 |
| nodejs-distroless-24-builder | wolfi-base | node, npm, node-gyp, gcc, make, python-3, git | 267 MB | amd64 |
| nodejs-distroless-22-builder | wolfi-base | node, npm, node-gyp, gcc, make, python-3, git | 267 MB | amd64 |
Pull figures are compressed layer totals for the latest amd64 manifest. Every
build also publishes an immutable <git-sha>-<DDMMYYYY> tag: pin
that in production, not latest. Full URIs are in the image catalog; sources are in the repositories.
Why the base decides the CVE count
Most "minimal" images still inherit a distribution's libc and toolchain packages, and those carry findings a scanner will report forever because upstream has no fix pending. Even the smallest Debian-based distroless variants sit on roughly seventeen permanently unfixed glibc and gcc advisories, so those images can never scan clean no matter how little you add on top.
A JRE does not need that much. libstdc++ is statically linked into HotSpot and zlib ships inside Temurin, so the runtime only needs glibc. Putting it on Chainguard's glibc-only distroless base, which is patched within days, is what takes the scan result to zero CVEs at every severity rather than merely "few".
The same reasoning drives the other two: nginx is compiled statically with ModSecurity and the OWASP Core Rule Set so it can run on a base that contains nothing at all, and Node.js is split into a full-toolchain builder and a runtime holding only the node binary and its shared libraries.
How the guarantee is kept
-
Nightly rebuilds
A scheduled GitHub Actions job rebuilds every image daily, so
latestis at most a day behind the newest Wolfi and Temurin patches. -
A scan that fails the build
Trivy runs under a strict gate: any CVE at any severity, including ones with no fix available, stops the publish. Scanning that only reports is not a gate.
-
Non-root, no shell
Nothing to exec into and no package manager to pull tools with. nginx runs as uid 65532 on port 8080 and is happy with a read-only root filesystem.
-
Signed provenance
Builds publish an OCI image index with attestation manifests attached, so what was scanned is traceable to what runs.
Using them
Java
Copy the jar onto the runtime; there is no build stage to strip out.
FROM ghcr.io/morningstar-sudo/jre-distroless-21:latest
COPY target/app.jar /app/app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"] Node.js
Compile with the builder, ship the runtime.
FROM ghcr.io/morningstar-sudo/nodejs-distroless-24-builder:latest AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
FROM ghcr.io/morningstar-sudo/nodejs-distroless-24-runtime:latest
COPY --from=build /app /app
CMD ["server.js"] nginx
The WAF is a runtime switch, not a separate image: a small static entrypoint picks the
ModSecurity config when IS_MS_ON is set.
# WAF off
docker run --rm -p 8080:8080 --read-only --tmpfs /tmp \
ghcr.io/morningstar-sudo/nginx-distroless:latest
# WAF on: ModSecurity v3 with the OWASP Core Rule Set
docker run --rm -p 8080:8080 --read-only --tmpfs /tmp \
-e IS_MS_ON=true ghcr.io/morningstar-sudo/nginx-distroless:latest