Server image build via Paketo Buildpacks + Application CDS
The application-server image is built from the server/application module with Spring Boot's
build-image goal (Paketo Cloud Native Buildpacks) and Application Class Data Sharing (CDS)
enabled. The hand-rolled Dockerfile was removed. Spring Boot AOT processing is off (see below).
Why
Production warm startup was ~13.3s/pod (post-#1281 baseline); across a 5-replica Coolify rolling-restart this is a perceivable p95 latency window. Spring's published CDS numbers on Spring MVC + Tomcat are ~1.5× faster startup (Spring blog 2024-08-29). Liquibase keeps its absolute share regardless; the realistic projection is ≥30% off the Spring portion of boot.
What the build does
builder-noble-java-tiny applies the Spring Boot buildpack. With BP_JVM_CDS_ENABLED=true the launcher runs once at build time (the "CDS training run") to load the bean-graph classes, archives them to /workspace/application.jsa, and bakes -XX:SharedArchiveFile=/workspace/application.jsa into the launcher. At runtime the JVM mmaps the archive instead of class-loading from JARs.
The training run boots under the cds-training profile (application-cds-training.yml), which disables Liquibase + JDBC-metadata probing and pins the Hibernate dialect so context refresh succeeds without a reachable Postgres. Coolify's runtime SPRING_PROFILES_ACTIVE=prod overrides the buildpack-baked default (Paketo writes env.launch/<KEY>.default, which yields to the runtime env).
Why not Spring AOT processing (spring.aot.enabled=true)
AOT evaluates conditional bean registration at build time. Hephaestus selects integrations and runtime roles from deployment configuration, so an image built in CI cannot safely fix those choices without omitting beans needed in production. CDS preserves runtime configuration while improving startup.
Why not GraalVM Native Image
8–12× CI build inflation, loss of JIT peak throughput on Hibernate workloads, no JFR/JVMTI/debugger, closed-world model breaks @ConditionalOn* runtime overrides, and three reflective deps (com.slack.api:bolt, kobylynskyi runtime, liquibase-core:5.x) lack published reachability metadata.
Builder pinning
server/application/pom.xml pins builder-noble-java-tiny + ubuntu-noble-run-tiny + the health-checker buildpack by
sha256 digest. Refresh:
docker buildx imagetools inspect paketobuildpacks/builder-noble-java-tiny:latest --format '{{.Manifest.Digest}}'
docker buildx imagetools inspect paketobuildpacks/ubuntu-noble-run-tiny:latest --format '{{.Manifest.Digest}}'
docker buildx imagetools inspect paketobuildpacks/health-checker:latest --format '{{.Manifest.Digest}}'
Bump as part of release cycles; the digest is the source of truth.
Container healthcheck on the distroless run image
On Docker Compose the container HEALTHCHECK is the only container-level health signal —
service_healthy gating and the docker compose ps column both depend on it. run-tiny has no
shell/wget and builder-noble-java-tiny bundles no probe, so server/application/pom.xml adds an explicit <buildpacks>
order. Specifying <buildpacks> replaces the builder's default order, so the java composite must
be re-listed (urn:cnb:builder:paketo-buildpacks/java) before appending
docker://paketobuildpacks/health-checker; BP_HEALTH_CHECKER_ENABLED=true opts it in. It contributes
the static, shell-free thc binary at /workspace/health-check, which the compose services invoke as an
exec-form HEALTHCHECK (THC_PORT/THC_PATH → actuator liveness/readiness). No health-check process
type is added, so the JVM-spawn-per-probe issue (health-checker#87) does not apply.
git CLI in the runtime image
GitDiffOperations previously shelled out to git; it was ported to JGit in the prerequisite commit, eliminating the runtime git dependency. The Paketo run image is used unmodified.
Rollback
Revert .github/workflows/ci-docker-build.yml (the use-buildpacks: true line) and re-add a Dockerfile. Coolify re-deploys the prior image SHA. Detection: Sentry release-tagged error spike, or Prometheus alert on application_ready_time_seconds > 15 for three consecutive deploys.
Operational checklist
- Coolify graceful shutdown —
application.ymlsetsSHUTDOWN_TIMEOUT:20s. Coolify's default container stop-grace is 10s; bump it to ≥25s in the deploy substrate so SIGTERM has time to drain in-flight requests. The Paketo launcherexecs the JVM; signal forwarding is native, notini. - JVM memory — do NOT set
MaxRAMPercentage,-Xmx, or-Xssin Coolify env. Paketo's memory calculator handles them. Override onlyBPL_JVM_HEAD_ROOMif needed. - CI build time — expect +60–120s per build vs the prior Dockerfile baseline (CDS training run dominates).
Sources
- Spring Boot 4 — Class Data Sharing how-to
- Spring Boot 4 — Packaging OCI Images
- Spring Boot 4 — Ahead-of-Time Processing
- OpenJDK JEP 483 — Ahead-of-Time Class Loading & Linking
- paketo-buildpacks/spring-boot
- paketo-buildpacks/spring-boot#571 — BP_JVM_CDS_ENABLED deprecation track
- Spring blog 2024-08-29 — CDS + Project Leyden