Skip to content

build(api): the api image is built by Cloud Native Buildpacks, with no shell and no Dockerfile - #2501

Merged
ExtraToast merged 2 commits into
mainfrom
feat/2449-api-buildpacks
Oct 15, 2026
Merged

ExtraToast merged 2 commits into
mainfrom
feat/2449-api-buildpacks

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Part of #2449
Part of #2440

What changes

The api image is built by Cloud Native Buildpacks: Gradle's bootBuildImage with Paketo's tiny Java builder. It has no shell and no Dockerfile; services/api/Dockerfile is gone. Architecture ADR-015 records the decisions and amends ADR-013.

  • Builder. paketobuildpacks/builder-noble-java-tiny:0.0.197 is pinned by digest in services/api/build.gradle.kts, and a new Renovate regex manager bumps it. The builder names its run image (ubuntu-noble-run-tiny) by version, so that one bump moves the JRE and the run image together. The image runs as the buildpack's cnb user (1002:1001).
  • AOT cache. BP_JVM_AOTCACHE_ENABLED turns it on. The training run gets TRAINING_RUN_JAVA_TOOL_OPTIONS with:
    • the training profile and spring.context.exit=onRefresh;
    • MaxRAM=2g and G1;
    • throwaway keys drawn per build. The buildpack prints that line, keys included, which is harmless because the keys are used for nothing else.
  • Flags baked into the image.
    • -XX:+UseG1GC is appended to every start's JAVA_TOOL_OPTIONS. Without it, the migrate Job's 1536 Mi would get Serial and ignore the cache.
    • SPRING_PROFILES_ACTIVE defaults to prod.
    • Native memory tracking and Spring Cloud Bindings, both Paketo defaults that nothing reads, are off.
  • Memory. Paketo's calculator always appends its own -Xmx. I measured that it silently overrides a -XX:MaxRAMPercentage, so the ticket's "heap percentage" would not work.
    • The Deployment sets JAVA_TOOL_OPTIONS=-Xmx1200M -XX:MaxDirectMemorySize=64M.
    • The migrate Job sets -Xmx640M -XX:MaxDirectMemorySize=64M.
    • The calculator sizes the rest from the limit and refuses to start a JVM whose total doesn't fit. I checked this with a 1900M heap in 2G: the start fails at the calculator.

CI

  • New .github/actions/build-api-image action:

    • restores the buildpacks' build cache (JRE and syft, about 340 MB) from the Actions cache, keyed on the builder's digest;
    • runs bootBuildImage into the runner's daemon;
    • smoke-boots the image.

    The launch cache is not saved: it is about 750 MB of the layers of the build that just ran.

  • Smoke boot. scripts/check-aot-cache.sh now reads each pod's limits and JAVA_TOOL_OPTIONS from the two manifests, so it can't drift from them, and boots under -XX:AOTMode=on.

  • Pull requests. Build Image (api) builds from api-static's jar, without compiling it again.

  • Main and release builds. The image is smoke-booted before it is pushed. The merge job then signs the api's manifest list with cosign, as before. A buildpack build records no BuildKit provenance or SBOM, so the merge job also:

    • describes the manifest list with syft;
    • attests that SBOM and the build provenance as GitHub artifact attestations, pushed to the registry beside it.

Runbooks without a shell

  • Break-glass starts java through the buildpack launcher (/cnb/lifecycle/launcher -- java -Xmx400M -cp runner.jar …), with its profile on the command line.
    • The old --spring.main.web-application-type=none never worked: SecurityConfig needs the servlet context. The run now uses --server.port=0 --management.server.port=0 instead.
    • Locally, that form reaches ApplicationReady, where the break-glass command runs. The command itself still needs production's database and Vault.
  • Restore rehearsal lists the live uploads from a short-lived pod that mounts api-storage read-only, so the rehearsal still writes nothing. Uploads are restored as the cnb user.
  • Bringup copies uploads through a pod mounting the claim, then restarts the api so its init container opens up the files.

Verified locally

  • Image: builds from a prebuilt jar with a bind-mounted cache. The AOT cache is 199 MB.
  • No shell: sh is not found.
  • Both pod shapes boot from the cache under AOTMode=on, with G1.
  • Break-glass launch form works as described above.
  • Lint: ktlint (including the build script), actionlint, the Renovate config validator, the Renovate regex against the file, and the Flux manifest check.

After merge

These criteria can only be checked in production, after a release:

The release also changes how heap is sized: an explicit 1200M, where the old image's MaxRAMPercentage=75 gave about 1.5G.

…o shell and no Dockerfile

bootBuildImage builds the api with Paketo's tiny Java builder, pinned by tag and digest and
bumped by a Renovate regex manager; the run image is the one the builder names. The buildpack
trains the AOT cache under the training profile, with throwaway keys drawn per build, and G1 is
appended to every start's JAVA_TOOL_OPTIONS. Native memory tracking and Spring Cloud Bindings,
Paketo defaults nothing reads, are off. services/api/Dockerfile is gone.

Paketo's memory calculator adds an -Xmx that overrides a heap percentage, so the Deployment and
the migrate Job name -Xmx and -XX:MaxDirectMemorySize=64M in JAVA_TOOL_OPTIONS, and the
calculator refuses a JVM that does not fit its limit. check-aot-cache.sh now reads those limits
and options from the manifests and boots the image with each under -XX:AOTMode=on.

A build-api-image action builds the image into the runner's daemon, with the buildpacks' build
cache restored from the Actions cache by builder digest, and smoke-boots it. The pull request
check builds from api-static's jar; the main and release builds push the image, and the merge
job attests the api's manifest list with an SBOM from syft and build provenance, pushed beside
it, as well as signing it.

The runbooks work without a shell: break-glass starts java through the buildpack launcher with
its profile on the command line, and on random ports, since the web-application-type=none it
used refused to start; the restore rehearsal lists live uploads from a read-only pod mounting
the storage claim, and restores them as the cnb user; bringup copies uploads through such a
pod. Architecture ADR-015 records the decisions and amends ADR-013.

Part of #2449
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Changes

Bucket Files Added Removed
platform 4 █ 37 ░ 11
build & config 10 ███████ 235 ░░░ 89
docs 4 ███ 100 ░ 4
total 18 +372 −104

No production or test lines added.

Coverage

Statements Branches Functions Lines
api 76.25% 70.75% 65.26% 78.54%
main 9f90bc7 76.25% 70.75% 65.26% 78.54%
Δ 0.00 0.00 0.00 0.00
frontend 92.81% 88.10% 90.34% 93.43%
main 9f90bc7 92.81% 88.10% 90.34% 93.43%
Δ 0.00 0.00 0.00 0.00
imaging 100.00% 100.00% 100.00% 100.00%
main 9f90bc7 100.00% 100.00% 100.00% 100.00%
Δ 0.00 0.00 0.00 0.00

Patch coverage: this pull request changes no line that coverage measures.

@ExtraToast
ExtraToast merged commit 9bd5dea into main Oct 15, 2026
49 checks passed
@ExtraToast
ExtraToast deleted the feat/2449-api-buildpacks branch October 15, 2026 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant