Skip to main content

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 shutdownapplication.yml sets SHUTDOWN_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 launcher execs the JVM; signal forwarding is native, no tini.
  • JVM memory — do NOT set MaxRAMPercentage, -Xmx, or -Xss in Coolify env. Paketo's memory calculator handles them. Override only BPL_JVM_HEAD_ROOM if needed.
  • CI build time — expect +60–120s per build vs the prior Dockerfile baseline (CDS training run dominates).

Sources