Files
spring-boot-demo/docker-images/docs/04-buildpacks.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

4.3 KiB

4. Buildpacks: what the builder decides for you

← 3. Layered jars · Index · Next: 5. jlink →

mvn spring-boot:build-image with no configuration used paketobuildpacks/builder-noble-java-tiny

  • the Spring Boot 4.1.1 default - and six of its 26 buildpacks took part: ca-certificates, bellsoft-liberica, syft, executable-jar, dist-zip, spring-boot.

What you get without writing a line of Dockerfile, from the build log and image-matrix.txt:

  • Java version from the jar. $BP_JVM_VERSION shows 21 as its default, then Using Java version 25 extracted from MANIFEST.MF - the buildpack reads Build-Jdk-Spec.
  • A non-root user, 1002:1001, and no shell (the tiny run image).
  • A reproducible image. Creation date is fixed (Docker shows "46 years ago" - 1980), so the same input gives the same digest.
  • A memory calculator that runs before the JVM and sets -Xmx, metaspace, code cache and thread stacks from the container's memory limit - see below, it can refuse to start the app.
  • An SBOM layer from Syft.
  • Spring Cloud Bindings on the classpath, which reads Kubernetes service bindings into Spring properties. BP_SPRING_CLOUD_BINDINGS_DISABLED=true removes it.

The size

345 MB on disk, and most of it is one layer:

276MB	Layer: 'jre', Created by buildpack: paketo-buildpacks/[email protected]

Temurin's 25 JRE is 200 MB. BellSoft's JRE carries a second VM: lib/client is 74 MB (its own libjvm.so plus two CDS archives) next to the 85 MB lib/server. BP_JVM_JLINK_ENABLED=true asks the buildpack to jlink a smaller runtime - with the caveat in chapter 5.

At 512 MiB the image does not start

buildpacks-memory.txt:

## 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)

The calculator reserves 1 MiB of stack for each of 250 threads plus a 240 MiB code cache and metaspace sized from the class count, before any heap. 512 MiB - a very common Kubernetes limits.memory - is not enough, and the container exits with code 82 before Java runs. The same jar in any of the Dockerfile-built images starts fine at 512 MiB.

Either give it more (-m 768m-Xmx190559K, a quarter of the limit) or tell it the truth about threads: BPL_JVM_THREAD_COUNT=50 → starts at 512 MiB with -Xmx133215K. With virtual threads, 250 platform threads is a generous assumption.

Behind a TLS-intercepting proxy

The Java buildpack downloads the JRE at build time, and the Spring Boot buildpack downloads Spring Cloud Bindings from Maven Central. Inside a corporate proxy both fail. Two profiles in the pom.xml handle it:

  • corporate-proxy - build container on the host network, HTTPS_PROXY passed through, and a ca-certificates binding so the build trusts the proxy's CA (Added 2 additional CA certificate(s) to system truststore in the log). With BP_EMBED_CERTS=false - the default - the CA is not baked into the runtime image.
  • no-maven-central - a dependency-mapping binding that points the buildpack at a local copy of the jar, keyed by the sha256 in the buildpack's buildpack.toml.

Both bindings are merged with combine.children="append", so the profiles compose: -Pcorporate-proxy,no-maven-central.

Two traps met on the way

  • The run image is pinned by tag inside the builder. The builder pulled as :latest asked for paketobuildpacks/ubuntu-noble-run-tiny:0.0.130; with pullPolicy=IF_NOT_PRESENT and only :latest present locally, the build still tried Docker Hub. Tagging the same digest as 0.0.130 fixed it.
  • -Dspring-boot.build-image.imageName is ignored if the pom sets <image><name> - explicit configuration beats the user property, so the "second" build silently overwrote the first image. The pom uses a ${buildpacks.image} property instead.