Files
spring-boot-demo/docker-images/docs/02-measuring-size.md
T
asmhatreandClaude Opus 5 644da9e65e Add docker-images: one Spring Boot 4 service packaged nine ways
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
2026-09-11 17:11:46 +00:00

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

image-matrix.txt:

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 run to 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/Paketo status.d/, or apk), 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.