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
This commit is contained in:
2026-09-11 17:11:46 +00:00
co-authored by Claude Opus 5
parent 86246dc860
commit 644da9e65e
52 changed files with 1308 additions and 1 deletions
@@ -0,0 +1,14 @@
## sbd/docker-images:aot-cache
Starting ImagesApplication v1.0.0 using Java 25.0.4 with PID 1 (/application/application.jar started by root in /applica
Started ImagesApplication in 1.657 seconds (process running for 1.966)
exit status while running: running
## sbd/docker-images:aot-cache-mismatch
[0.007s][warning][aot] The AOT cache was created by a different version or build of HotSpot
[0.007s][error ][aot] An error has occurred while processing the AOT cache. Run with -Xlog:aot for details.
[0.008s][error ][aot] Loading static archive failed.
[0.008s][error ][aot] Unable to map shared spaces
Starting ImagesApplication v1.0.0 using Java 25.0.4.1 with PID 1 (/application/application.jar started by nonroot in /ap
Started ImagesApplication in 3.564 seconds (process running for 4.05)
exit status while running: running
@@ -0,0 +1,21 @@
# docker history sbd/docker-images:buildpacks
69B Buildpacks Process Types
1.94kB Buildpacks Launcher Config
2.93MB Buildpacks Application Launcher
0B Application Slice: 5
14.2kB Application Slice: 4
0B Application Slice: 3
402kB Application Slice: 2
25.9MB Application Slice: 1
577kB Software Bill-of-Materials
3B Layer: 'web-application-type', Created by buildpack: paketo-buildpacks/[email protected]
77.4kB Layer: 'spring-cloud-bindings', Created by buildpack: paketo-buildpacks/[email protected]
4.01MB Layer: 'helper', Created by buildpack: paketo-buildpacks/[email protected]
11B Layer: 'classpath', Created by buildpack: paketo-buildpacks/[email protected]
276MB Layer: 'jre', Created by buildpack: paketo-buildpacks/[email protected]
214B Layer: 'java-security-properties', Created by buildpack: paketo-buildpacks/[email protected]
5.48MB Layer: 'helper', Created by buildpack: paketo-buildpacks/[email protected]
5.18MB Layer: 'helper', Created by buildpack: paketo-buildpacks/[email protected]
519B
191B
24.9MB
@@ -0,0 +1,16 @@
## docker run -m 512m
unable to calculate memory configuration
fixed memory regions require 595872K which is greater than 512M available for allocation: -XX:MaxDirectMemorySize=10M, -XX:MaxMetaspaceSize=83872K, -XX:ReservedCodeCacheSize=240M, -Xss1M * 250 threads
ERROR: failed to launch: exec.d: failed to execute exec.d file at path '/layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator': exit status 1
state: exited (exit 82)
## docker run -m 768m
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx190559K -XX:MaxMetaspaceSize=83872K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 768M, Thread Count: 250, Loaded Class Count: 12394, Headroom: 0%)
Started ImagesApplication in 3.884 seconds (process running for 4.423)
state: running (exit 0)
## docker run -m 512m -e BPL_JVM_THREAD_COUNT=50
Calculated JVM Memory Configuration: -XX:MaxDirectMemorySize=10M -Xmx133215K -XX:MaxMetaspaceSize=83872K -XX:ReservedCodeCacheSize=240M -Xss1M (Total Memory: 512M, Thread Count: 50, Loaded Class Count: 12394, Headroom: 0%)
Started ImagesApplication in 3.846 seconds (process running for 4.353)
state: running (exit 0)
@@ -0,0 +1,14 @@
# Spring Boot 4.1.1 application (webmvc + actuator + validation + prometheus), fat jar 25M
# Docker Engine 29.4.3, classic overlay2 store. 3 startup runs each, median shown.
# 'on disk' = docker image inspect .Size (uncompressed). 'pushed' = layer blobs + config in the registry (compressed).
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
@@ -0,0 +1,31 @@
# The same eleven images, loaded from the same tarballs, listed by `docker images` under the two
# Docker Engine 29.4.3 image stores. Captured once by hand (switching stores needs a daemon
# restart); the commands are in docs/02-measuring-size.md.
## containerd image store (daemon.json: {"features":{"containerd-snapshotter":true}} - the default for new installs)
REPOSITORY:TAG IMAGE ID SIZE
paketobuildpacks/ubuntu-noble-run-tiny:latest 5fb8a81f3f56 38.7MB
eclipse-temurin:25-jre 211325bca3fd 496MB
eclipse-temurin:25-jdk 00eacca34899 608MB
eclipse-temurin:25-jre-alpine 31a7faf76302 305MB
bellsoft/liberica-openjre-debian:25-cds 78b8d1ba9172 505MB
quay.io/prometheus/prometheus:v3.14.0 0230c2ba4c1c 372MB
busybox:1.37 e0b4f7eca906 6.77MB
registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.12.0 b8e2bc8f8de9 131MB
paketobuildpacks/builder-noble-java-tiny:latest b54d7bce7d3c 1.26GB
gcr.io/distroless/java-base-debian13:nonroot fe682f609c79 43.1MB
gcr.io/distroless/java25-debian13:nonroot 3ad8b3cc79ab 305MB
## classic overlay2 store (daemon.json: {"features":{"containerd-snapshotter":false}})
REPOSITORY:TAG IMAGE ID SIZE
paketobuildpacks/ubuntu-noble-run-tiny:latest 3f2697c8829c 24.9MB
eclipse-temurin:25-jre a33388e97452 352MB
eclipse-temurin:25-jdk ca0436742d32 430MB
eclipse-temurin:25-jre-alpine 0988c057a1e4 226MB
bellsoft/liberica-openjre-debian:25-cds aea7393405da 356MB
quay.io/prometheus/prometheus:v3.14.0 31c1e0aacb3a 261MB
busybox:1.37 db287cb6be81 4.42MB
registry.k8s.io/prometheus-adapter/prometheus-adapter:v0.12.0 3bba96c2e4e6 89.3MB
paketobuildpacks/builder-noble-java-tiny:latest d835ec7c8f6e 868MB
gcr.io/distroless/java-base-debian13:nonroot 9bc10980a41f 26.5MB
gcr.io/distroless/java25-debian13:nonroot be8c5fbd580c 226MB
@@ -0,0 +1,30 @@
# jlink modules chosen by jdeps alone:
java.base java.compiler java.datatransfer java.desktop java.instrument java.logging java.management java.naming java.net.http java.prefs java.scripting java.security.jgss java.security.sasl java.sql java.transaction.xa java.xml jdk.jfr jdk.unsupported
# WARN lines at startup of the jdeps-only image:
i.m.c.i.binder.jvm.JvmGcMetrics : GC notifications will not be available because com.sun.management.GarbageCollectionNotificationInfo is not present
i.m.c.i.binder.jvm.JvmGcMetrics : GC notifications will not be available because no GarbageCollectorMXBean of the JVM provides any. GCs=[G1 Young Generation, G1 Concurrent GC, G1 Old Generation]
# Metric names: full JRE 61, jdeps-only 46, jdeps + jdk.management 58
# Present with the full JRE, missing from the jdeps-only image:
jvm_gc_concurrent_phase_time_seconds_count
jvm_gc_concurrent_phase_time_seconds_max
jvm_gc_concurrent_phase_time_seconds_sum
jvm_gc_live_data_size_bytes
jvm_gc_max_data_size_bytes
jvm_gc_memory_allocated_bytes_total
jvm_gc_memory_promoted_bytes_total
jvm_gc_pause_seconds_count
jvm_gc_pause_seconds_max
jvm_gc_pause_seconds_sum
process_cpu_time_ns_total
process_cpu_usage
process_files_max_files
process_files_open_files
system_cpu_usage
# Missing from jdeps + jdk.management: 3
jvm_gc_concurrent_phase_time_seconds_count
jvm_gc_concurrent_phase_time_seconds_max
jvm_gc_concurrent_phase_time_seconds_sum
# (jvm_gc_concurrent_phase_time_* is registered lazily, after the first G1 concurrent cycle -
# whether it appears within a few seconds of startup varies run to run, in either image.)
@@ -0,0 +1,12 @@
# docker stop sends SIGTERM to PID 1, waits 10 s, then SIGKILL. Exit code 143 = the JVM handled
# SIGTERM (128+15). 137 = it was killed (128+9).
layered-jre PID 1: java -jar application.jar docker stop: 244 ms exit code: 143 graceful-shutdown log lines: 1
shell-form PID 1: /bin/sh -c java -jar /app/app.jar docker stop: 10160 ms exit code: 137 graceful-shutdown log lines: 0
shell-form-alpine PID 1: java -jar /app/app.jar docker stop: 219 ms exit code: 143 graceful-shutdown log lines: 1
shell-form-wrapper PID 1: /bin/sh -c echo "starting revision $(date +%s)" && java -j docker stop: 10176 ms exit code: 137 graceful-shutdown log lines: 0
layered-distroless PID 1: (image has no cat; ENTRYPOINT is exec form) docker stop: 256 ms exit code: 143 graceful-shutdown log lines: 1
# What /bin/sh is in each base image:
eclipse-temurin:25-jre -> /usr/bin/dash
eclipse-temurin:25-jre-alpine -> /bin/busybox
@@ -0,0 +1,14 @@
# One-line code change (BuildInfo.REVISION 1 -> 2), every variant rebuilt.
# 'new layers' = layer digests in revision 2 that revision 1 did not have: what a push uploads
# and what a node that already runs revision 1 downloads.
variant new layers bytes to push image total share
fatjar-jdk 1 of 7 23422K 178M 13.1%
fatjar-jre 1 of 7 23422K 144M 16.2%
layered-jre 1 of 11 6K 144M 0.0%
layered-alpine 1 of 10 6K 98M 0.0%
layered-distroless 1 of 39 6K 97M 0.0%
jlink-distroless 1 of 39 6K 76M 0.0%
aot-cache 2 of 12 15323K 160M 9.6%
buildpacks 2 of 20 64K 125M 0.1%
jib 1 of 10 2K 144M 0.0%