The Keycloak Upgrade Ledger

Every upgrade we have rehearsed, with the environment stated and the clock running.

2026-08-26 · run 2026-08-26-runB-optimized-image

Run B: the pre-built image halves the heap floor and turns S7's silent failure loud

Upgrade
26.0.0 → 26.7.1
Scale
1,000 users, typical, baseline-26.0.0-typical-1k
Database
PostgreSQL 16, single node, container
Topology
single container
Host
Hetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe
Condition
routine — no adverse condition applied

Why this run existed

docker-compose.yml says, in a comment, why the lab does not use --optimized: start --optimized is valid, but when Keycloak detects a config change it re-runs the build step with the same argv and build rejects --optimized. The supported route is to run kc.sh build at image build time. Nobody had done it, which left the lab's single biggest divergence from a production deployment untested — and left S5's published heap floor carrying a caveat.

Environment

FieldValue
Keycloak from → to26.0.0 (stock image) → 26.7.1 (pre-built image)
Imagekclab/keycloak-optimized:26.7.1, built by fixtures/optimized/Dockerfilekc.sh build with KC_DB=postgres, health and metrics on; 772 MB against the stock 766 MB
Start modestart --optimized --http-enabled=true
DatabasePostgreSQL 16, single node, container
Postgres tuningstock
Dataset scale1,000 users, typical, baseline-26.0.0-typical-1k
Topologysingle container
HostHetzner CCX33 (fsn1) — 8 vCPU dedicated, 30 GB RAM, local NVMe
JVM heap-Xms1g -Xmx4g except in B2, where it is the variable
Lab changefixtures/optimized/Dockerfile, bin/build-optimized.sh, docker-compose.optimized.yml, docker-compose.optimized-noprov.yml

Outcome

Verification

ClaimPrimary sourceChecked
The optimized image runs the migration normally211 changelog rows, migration_model 26.7.1
Upgrade elapsed 17s → 11supgrade.sh output, same baseline both runs
The saving is the build step, not the migrationQuarkus started in and Bootstrap identical to within 0.03s
Heap floor is (64m, 96m] on the optimized image96m READY, 64m FAILED with an OOM line
S5's floor of (128m, 160m] was the build step128m FAILED stock, READY optimized
A runtime-only provider jar prevents startupB3a, message quoted
A providers bind mount shadows the image's ownB3b-i vs B3b-ii; ls inside the container empty vs populated
The provider registers when built in and unmounted/admin/serverinfo lists lab-event-listener
Heap floor at 2M users under --optimizednot tested — B2 ran at 1k. S5 showed the stock floor is scale-independent; that is an assumption here, not a measurement
Whether a broken provider built into an optimized image still fails lazilynot tested — only the working jar was built in
--optimized across a version boundary (build at 26.0.0, start at 26.7.1)not tested — the optimized image existed only for 26.7.1, so the 26.0.0 side of B1 ran stock

← All runs