A Cloud Native Buildpacks builder optimised for Java (JVM and GraalVM Native Image) that produces minimal, secure application images using Google Distroless as the runtime base.
Inspired by the paketo-buildpacks/builder-jammy-tiny philosophy: only the dependencies that Java actually needs, nothing more.
| Component | Base image | Purpose |
|---|---|---|
| Build stack | ubuntu:24.04 |
Full toolchain for compiling Java apps |
| Run stack | gcr.io/distroless/cc:nonroot |
Minimal, shell-free Java runtime |
| Builder | CNB lifecycle + Paketo Java Buildpacks | Orchestrates builds |
The run image has no shell, no package manager, no debug tools — drastically reducing the attack surface of every Java container built with this builder.
| Language | Buildpack |
|---|---|
| Java / Spring Boot | paketo-buildpacks/java |
| Java Native Image (GraalVM) | paketo-buildpacks/java-native-image |
Prerequisites: Docker ≥ 20.10, pack CLI ≥ 0.33
pack build my-java-app \
--builder ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny:latest \
--path ./my-java-apppack config default-builder ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny:latest
pack build my-java-appConfigure once in pom.xml:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny:latest</builder>
<pullPolicy>IF_NOT_PRESENT</pullPolicy>
<env>
<BP_JVM_JLINK_ENABLED>true</BP_JVM_JLINK_ENABLED>
</env>
</image>
</configuration>
</plugin>Then build:
mvn spring-boot:build-image
# or override the builder without changing pom.xml:
mvn spring-boot:build-image \
-Dspring-boot.build-image.builder=ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny:latestImages are published to GHCR on every push to main and on version tags. The run image is also rebuilt nightly to pick up base image security patches. GHCR images include SLSA build provenance attestations.
| Image | Registry |
|---|---|
ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny |
GHCR |
ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny/build |
GHCR |
ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny/run |
GHCR |
Prerequisites: Docker with buildx, pack CLI, make
make build-stack # Build + push multi-arch stack images (amd64, arm64)
make build-builder # Assemble the CNB builder image
make test # Run smoke + integration tests
make test-smoke # Run smoke tests only (fast)Note:
make build-stackusesdocker buildx build --pushto produce multi-arch images (linux/amd64+linux/arm64). It pushes directly to the registry — local loading of multi-platform images is not supported by Docker. SetPLATFORMS=linux/amd64to restrict to a single architecture.
All samples in samples/ expose / and /health on port 8080.
| Sample | Language |
|---|---|
samples/java |
Java 25 / Spring Boot |
samples/java-native-image |
Java 25 / GraalVM Native Image |
Build a sample:
pack build my-app \
--path ./samples/java \
--builder ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny:latest├── builder.toml # CNB builder configuration
├── Makefile # Local build automation
├── openrewrite/rewrite.yml # Shared OpenRewrite recipes
├── benchmarks/budgets.json # Performance SLO budgets
├── stack/
│ ├── build/Dockerfile # Build stack (Ubuntu 24.04)
│ └── run/Dockerfile # Run stack (Google Distroless)
├── samples/
│ ├── java/ # Spring Boot (JVM)
│ └── java-native-image/ # Spring Boot (GraalVM Native Image)
├── tests/
│ ├── integration/ # End-to-end builder tests
│ └── smoke/ # Fast label + config validation
└── .github/
├── dependabot.yml
└── workflows/
├── build-and-push.yml
├── test.yml
├── quality-gates.yml
├── security-scan.yml
├── scorecard.yml
├── benchmark.yml
├── openrewrite.yml
├── dependency-policy-review.yml
└── release.yml
| Workflow | Trigger | Description |
|---|---|---|
| Build and Push | push to main, version tags |
Build stack images + builder, push to GHCR |
| Integration Tests | push, pull_request | Smoke tests → integration tests (pack + mvn) |
| Quality Gates | push, pull_request | ShellCheck, actionlint, markdownlint, OpenRewrite, Checkstyle, tests |
| Security Scan | push, pull_request, weekly | Hadolint, Trivy filesystem + image scans |
| OSSF Scorecard | push to main, weekly |
Supply-chain security posture analysis |
| Benchmark | after Build and Push, weekly | Build times, image sizes, runtime metrics |
| OpenRewrite | monthly, manual | Auto-apply code cleanup recipes, create PRs |
| Dependency Policy Review | quarterly, manual | Governance audit checklist |
| Release | version tags (v*) |
GitHub Release with pull instructions |
Every pull request must pass the checks below. Each check exists for a single, specific reason — there is no overlap.
| Check | Workflow | What it proves |
|---|---|---|
| ShellCheck | Quality Gates | Shell scripts follow best practices and avoid common bugs |
| actionlint | Quality Gates | GitHub Actions workflow syntax is valid |
| markdownlint | Quality Gates | Documentation formatting is consistent |
| OpenRewrite dry-run | Quality Gates | Code matches the shared cleanup recipe (no uncommitted rewrites) |
| Checkstyle | Quality Gates | Java source follows Google style conventions |
Unit tests (mvn test) |
Quality Gates | Sample endpoint contracts (/ and /health) hold |
| Hadolint | Security Scan | Dockerfiles follow best-practice lint rules |
| Trivy (filesystem + image) | Security Scan | No known CVEs in dependencies or built images |
| Smoke tests | Integration Tests | Stack image labels, UIDs, and builder.toml structure are correct |
| Integration tests | Integration Tests | Builder produces a runnable container that responds on / |
If a check does not appear in this table, it should not be in CI. If a claim appears in documentation, it should map to one of these checks.
The run image (gcr.io/distroless/cc:nonroot) provides:
- No shell — attackers cannot execute shell commands
- No package manager — nothing installable at runtime
- Non-root user (uid 1002) by default
- C++ runtime (
libstdc++,libgcc) included for JVM and native binary support
Automated scanning on every push:
- Trivy — CVE scanning of container images and filesystem
- Hadolint — Dockerfile best-practice linting
- OSSF Scorecard — Supply-chain security posture (weekly)
SARIF reports are published to the Security tab.
See SECURITY.md for the vulnerability disclosure policy.
| Scenario | Recommendation |
|---|---|
| Spring Boot REST API / microservice | Yes — ideal workload, minimal footprint |
| Spring Boot with GraalVM Native Image | Yes — fastest startup, smallest image |
| App that needs outbound HTTPS/TLS calls via system SSL | No — the run image strips OpenSSL and CA certificates; the JVM's built-in TLS stack works, but Native Image binaries linking against system libssl will fail |
| App that writes to the local filesystem at runtime | Caution — limited writable paths; design for stateless operation |
| App that requires a shell for debugging or exec-ing into the container | No — the run image has no shell by design |
| Non-Java workloads (Go, Rust, Node.js) | No — this builder only ships Java and Java Native Image buildpacks |
The trimmed run image includes glibc, libstdc++, and libgcc_s — enough for the JVM and ahead-of-time compiled Native Image binaries. The following components are intentionally removed to minimise size and attack surface:
- OpenSSL (
libssl3,libcrypto3) and CA certificates libgomp,libitm,libatomic- Full timezone database (only UTC is included; Java uses its own bundled TZDB)
If your workload depends on system-level TLS or these libraries, use the standard paketobuildpacks/builder-jammy-tiny builder instead, or open a feature request for a TLS-compatible run image variant.
One key benefit of smaller, distroless images is lower infrastructure cost. Here's how to measure and act on it:
| Metric | What to watch | Action threshold |
|---|---|---|
| Image size | Compressed pull size (check docker manifest inspect) |
Alert if >50% larger than baseline |
| Container RSS | Resident Set Size via docker stats or Prometheus container_memory_rss |
Alert if steady-state exceeds requested memory ×0.8 |
| JVM heap | -XX:MaxRAMPercentage (default 25%) of container memory limit |
Tune if GC pause time or OOM kills increase |
| Startup time | Time from container start to first HTTP 200 on /health |
JVM: <10s, Native Image: <1s |
| CPU throttling | container_cpu_cfs_throttled_seconds_total in Prometheus |
Increase CPU limit or optimise hot paths |
| Image pull time | CI or Kubernetes pull duration | Smaller images = faster rollouts and autoscaling |
| Mode | Suggested starting limits | Expected image size |
|---|---|---|
| JVM (jlink) | 512 Mi memory, 500m CPU | ~100–140 MB |
| Native Image | 128 Mi memory, 250m CPU | ~80–120 MB |
If you notice resource consumption growing over time:
- Check for memory leaks — compare heap dumps across releases
- Review dependency growth — new libraries add startup time and memory; use
mvn dependency:treeto audit - Profile GC behaviour — switch to ZGC or Shenandoah if pause times matter
- Consider Native Image — for workloads where startup time and baseline memory dominate cost
- Track image size in CI — the Benchmark workflow already does this; set an alert threshold
# Build-time benchmark (3 iterations)
for i in 1 2 3; do
time pack build bench-app \
--path ./samples/java \
--builder ghcr.io/patbaumgartner/distroless-buildpack-builder-java-tiny:latest \
--clear-cache
done
# Image size comparison
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep bench-appSee CONTRIBUTING.md.
See CODE_OF_CONDUCT.md.
See SUPPORT.md.
Apache 2.0 — see LICENSE.