Companion code for "Dockerizing Spring Boot 4: Layered Jars, Buildpacks, Distroless and Image Size Benchmarks". Fat jar on JDK and JRE, layered jar on Debian, Alpine and distroless, jlink, the JDK 25 AOT cache, Paketo buildpacks and Jib, each measured for size on disk and pushed, rebuild delta, startup, user and shell. Also PID 1 and signal handling, the jdeps module gap, AOT cache mismatches and buildpacks memory calculation. Transcripts in docs/output/, regenerated by scripts/run-all.sh. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
3.6 KiB
2. Measuring size
← 1. The variants · Index · Next: 3. Layered jars →
"Image size" means at least three different numbers:
| Number | What it is | How it is measured here |
|---|---|---|
| on disk | uncompressed layers, what a node stores | docker image inspect -f '{{.Size}}' on the classic overlay2 store |
| pushed | compressed layer blobs + config, what a registry stores and a cold node downloads | sum of layers[].size + config.size from crane manifest |
| rebuild delta | the blobs a push actually uploads after a change | chapter 3 |
measure.sh pushes every variant to a throwaway registry
(crane registry serve --address localhost:5000 - in-memory, no Docker Hub account needed) and
reads the manifests back.
docker images disagrees with itself
Docker Engine 29 can use two image stores. The same eleven tarballs, loaded into each
(image-store-size-difference.txt):
| Image | containerd store | classic overlay2 |
|---|---|---|
eclipse-temurin:25-jre |
496 MB | 352 MB |
gcr.io/distroless/java25-debian13:nonroot |
305 MB | 226 MB |
paketobuildpacks/builder-noble-java-tiny |
1.26 GB | 868 MB |
The containerd store's figure is larger across the board. Size comparisons from blog posts - or between two colleagues' laptops - are only comparable if they used the same store, and the containerd store is the default for new Docker installations. This project measures on the classic store because its number is the unpacked size alone.
The result
variant on disk pushed layers user shell packages ready ms Started RSS JVM
fatjar-jdk 456M 178M 7 root yes 115 (dpkg) 5362 3.932s 169.5MiB Eclipse 25.0.4
fatjar-jre 377M 144M 7 root yes 106 (dpkg) 5091 3.944s 182.1MiB Eclipse 25.0.4
layered-jre 377M 144M 11 root yes 106 (dpkg) 4272 3.356s 169.5MiB Eclipse 25.0.4
layered-alpine 251M 98M 10 root yes 44 (apk) 4634 3.775s 145.9MiB Eclipse 25.0.4
layered-distroless 252M 97M 39 65532 no 25 (status.d) 4118 3.254s 164.3MiB Eclipse 25.0.4.1
jlink-distroless 119M 76M 39 65532 no 24 (status.d) 4386 3.39s 165.7MiB Eclipse 25.0.4
aot-cache 440M 160M 12 root yes 106 (dpkg) 2412 1.638s 174.6MiB Eclipse 25.0.4
buildpacks 345M 125M 20 1002:1001 no 10 (status.d) 4726 3.615s 183.6MiB BellSoft 25.0.4
jib 377M 144M 10 root yes 106 (dpkg) 4323 3.457s 162.5MiB Eclipse 25.0.4
- ready ms is
docker runto the first 200 from/actuator/health/readiness; Started is Spring Boot's own figure. Median of three on a 2-vCPU VM - treat differences under ~0.3 s as noise. - packages counts OS packages from the image filesystem without running anything in it (dpkg
status, distroless/Paketostatus.d/, orapk), which works for images with no shell. - Every Temurin-based variant runs as root: the official images set no
USER.
A note on method: the first run of this table had distroless starting two seconds slower than
everything else. A k3s image import was running on the same two CPUs at the time. Nothing else runs
during measure.sh now.