konzept vertraulich owner: matus
Natives Build-Tooling — was steht, was bewusst zurückgestellt ist
Natives Build-Tooling — was steht, was bewusst zurückgestellt ist
Status: Probebau am 2026-08-10 gelaufen und grün — GG-DEVOPS-0003.3 ist damit erfüllt
· Offen: ggf. ein Promotions-Skript, UI-Image · Auslöser: globale Regel GG-DEVOPS-0003
Was gilt
Zwirn hat am 2026-08-09 GG-DEVOPS-0003 beschlossen
(../../../zwirn/guidelines/devops/native-images.md,
Zwirn-ADR-0026):
- jeder nicht-lokale Einsatz — Staging, Produktion und ausdrücklich auch jede Pilotpraxis-Instanz — muss als GraalVM/Mandrel-Native-Image laufen. JVM nicht-lokal ist ein Regelverstoß.
- lokal bleibt JVM der Standard (
docker-compose.yml). Native lokal ist nur ein Opt-in-Overlay.
Für dieses Produkt ist der Pilot-Fall der relevante: sobald tuxametrics irgendwo läuft, wo nicht
entwickelt wird, greift die Pflicht.
Was steht (2026-08-09)
| Artefakt | Zweck |
|---|---|
../../tuxametrics-laborauswertung-service/Dockerfile.native |
Reines Kopier-Dockerfile auf ubi9-minimal, erwartet ein auf dem Host gebautes target/*-runner |
../../tuxametrics-platform-service/Dockerfile.native |
dito (neu 2026-08-10) |
../../tuxametrics-virtual-identity-service/Dockerfile.native |
dito (neu 2026-08-10) |
../../docker-compose.native.yml |
Opt-in-Overlay für alle drei Quarkus-Module: eigener Image-Tag (…-native) + curl-Healthcheck statt wget. tuxametrics-process-hub-cockpit fehlt dort bewusst (siehe Punkt 4) |
Der eigene Image-Tag ist der Punkt, an dem die Referenzimplementierung (dl-controlling/dot) einmal
teuer gelernt hat: teilen sich Basis und Overlay einen Tag, erwischt ein docker compose up -d ohne
Overlay den nativen Container mit dem wget-Healthcheck der Basisdatei — ubi9-minimal hat kein
wget, der Container läuft einwandfrei und gilt trotzdem dauerhaft als unhealthy.
Vorarbeit, die schon vorher da war: <profile><id>native</id></profile> im pom und die Zeile
quarkus.native.additional-build-args=--initialize-at-run-time=…util.NanoId in
application.properties (NanoId hält ein statisches SecureRandom, das native-image nicht im
Image-Heap duldet).
Was offen ist — ehrlich getrennt
1. Der Probebau ist noch nicht gelaufen. Erledigt am 2026-08-10 — mit einem Fund.
GG-DEVOPS-0003.3 verlangt „mindestens einmal nachweislich gebaut". Das ist jetzt der Fall. Die
Aussage „nativ baubar" ist keine Behauptung mehr. Der Lauf hat allerdings genau das gebracht, was
oben befürchtet wurde — nur an einer anderen Stelle als erwartet.
Lauf 1 — der bis dahin dokumentierte Befehl, unverändert:
mvn -f tuxametrics-laborauswertung-service/pom.xml package -DskipTests -Pnative \
-Dquarkus.native.container-build=true \
-Dquarkus.native.builder-image=quay.io/quarkus/ubi9-quarkus-mandrel-builder-image:jdk-25
Ergebnis: BUILD FAILURE nach 5:04 min. Exit-Code des Builder-Containers 137 — SIGKILL durch
den OOM-Killer. Quarkus' eigene Diagnose im Log: „The Native Image build process ran out of memory."
Die Maschine hatte 16,5 GB Docker-Speicher und 22 CPUs zur Verfügung; es lag also nicht an einem zu
kleinen Rechner. native-image bemisst seinen Heap an der sichtbaren Maschinengröße und läuft dann
gegen das Limit des Containers, in dem es selbst steckt.
Lauf 2 — derselbe Befehl plus einen Heap-Deckel:
… -Dquarkus.native.native-image-xmx=6g
Ergebnis: BUILD SUCCESS in 3:20 min (Image-Erzeugung 2:38 min, 11,3 s in 502 GCs, Peak RSS
5,99 GB, CPU-Last 11,79). Erzeugtes Binary:
tuxametrics-laborauswertung-service/target/tuxametrics-laborauswertung-service-0.1.0-SNAPSHOT-runner,
95 MiB (99.564.600 Bytes).
Bemerkenswert: keine einzige native-Inkompatibilität. Die oben vermuteten Klassiker — Reflection
auf Jackson-Records, dynamische Proxies der REST-Clients — traten nicht auf. Der einzige Stolperstein
war Speicher, und der --initialize-at-run-time-Eintrag für NanoId (siehe unten) hat offenbar
genau das abgedeckt, wofür er gedacht war.
Konsequenz, bereits umgesetzt: Der Deckel steht jetzt fest im native-Profil
(tuxametrics-laborauswertung-service/pom.xml) statt als Flag, den jeder Aufrufer kennen muss:
<quarkus.native.native-image-xmx>6g</quarkus.native.native-image-xmx>
Damit funktioniert der oben dokumentierte Befehl unverändert. Wer ihn ohne das Profil aufruft (CI mit eigener Konfiguration), muss den Wert selbst setzen — 6 GB ist der belegte Bedarf plus etwas Luft, nicht ein geratener Wert (Peak RSS 5,99 GB). Wird der Service größer, ist das die erste Zahl, die nachzuziehen ist; das Symptom ist wieder Exit 137, nicht eine verständliche Fehlermeldung.
Was der Lauf NICHT gezeigt hat, und das ist wichtig: Das Binary wurde erzeugt, aber nie gestartet. „Baut nativ" und „läuft nativ" sind zwei Aussagen; native-image verschiebt Fehlerklassen gern von der Bau- in die Startzeit (fehlende Reflection-Registrierung schlägt erst beim ersten Request zu). Der nächste ehrliche Schritt ist deshalb:
docker compose -f docker-compose.yml -f docker-compose.native.yml build tuxametrics-laborauswertung-service
docker compose -f docker-compose.yml -f docker-compose.native.yml up -d --no-deps tuxametrics-laborauswertung-service
# und dann mindestens den Referenzfall einmal durchspielen, nicht nur /q/health abfragen
Das ist am 2026-08-10 nicht geschehen — es braucht einen laufenden Postgres und eine
.env mit ZWIRN_M2, und der Stack ist ohne entschiedene Authority ohnehin nur eingeschränkt
lauffähig (siehe capability-authority.md).
1b. Nachtrag 2026-08-10, zweiter Durchgang: zwei neue Module, und der Speicher wurde zur eigentlichen Geschichte.
Mit tuxametrics-platform-service und tuxametrics-virtual-identity-service
(ADR-0005) sind aus einem Quarkus-Modul
drei geworden. Beide neuen tragen denselben Heap-Deckel im native-Profil, aus demselben Grund.
| Modul | Ergebnis | Deckel | Zeit | Binary |
|---|---|---|---|---|
tuxametrics-platform-service |
✅ BUILD SUCCESS (2. Versuch) | 6g |
3:12 min | 88 MiB |
tuxametrics-virtual-identity-service |
✅ BUILD SUCCESS (3. Versuch) | 4g |
4:01 min | 89 MiB |
tuxametrics-laborauswertung-service |
✅ BUILD SUCCESS (nach 1 Fehlversuch mit 6g) |
4g |
3:31 min | 95 MiB |
Alle drei Module bauen nativ. Das war vor diesem Durchgang für zwei von dreien eine Behauptung.
Was der zweite Durchgang gezeigt hat und der erste nicht zeigen konnte: der Fehlerfall ist nicht
nur „native-image bemisst seinen Heap an der Maschine", sondern wie wenig Reserve nötig ist, damit es
kippt. Auf derselben Maschine, mit demselben Deckel 6g:
platform-serviceallein gestartet: erfolgreich.vidundlaborauswertungdirekt nacheinander gestartet: beide Exit 137.viddanach allein noch einmal: wieder Exit 137.
Der Unterschied war nicht der Code, sondern dass durchgehend 59 Container aus drei anderen Produkten
(evenwes, evepop, secondbrain) rund 10 GB des 15,4-GB-Docker-Budgets hielten. Ein
-Xmx6g-Builder-Container passt in die verbleibenden ~4,9 GB schlicht nicht — dass der
platform-service-Lauf durchkam, war ein günstiger Moment, keine Regel.
Und dann der eigentliche Fund, der gegen die Intuition läuft: ein KLEINERER Deckel hat geholfen.
vid scheiterte mit -Xmx6g auch allein gestartet (5:17 min, Exit 137). Derselbe Bau mit
-Xmx4g: BUILD SUCCESS in 4:01 min, Binary 89 MiB. Frei waren zu dem Zeitpunkt ~4,9 GB.
Das ist nicht paradox, sondern die Auflösung einer Verwechslung, die auch in Quarkus' eigener Fehlermeldung steckt („consider increasing the Xmx value"):
-Xmxist der Java-Heap des Builders. Wird er zu klein, bricht native-image mit einer ordentlichenOutOfMemoryError-Meldung ab.- Exit 137 ist etwas anderes: SIGKILL von außen. Der Builder-Container wurde vom Host getötet, weil
er sich mehr genommen hat, als noch da war. Ein größerer
-Xmxmacht das schlimmer, nicht besser.
Faustregel, belegt statt geraten: der Deckel muss unter den tatsächlich freien Speicher passen,
nicht über den Bedarf. Bei Exit 137 zuerst -Xmx senken (oder Speicher freimachen) — nicht erhöhen.
Der Preis eines kleineren Heaps ist mehr GC, also etwas längere Bauzeit; hier war er nicht einmal
messbar, weil der 6-GB-Lauf ohnehin abbrach.
Warum trotzdem 6g in den Profilen steht: es ist der auf einer freien Maschine gemessene Bedarf
(Peak RSS 5,99 GB in Durchgang 1) und damit der schnellste Lauf. Wer auf einer belegten Maschine baut,
überschreibt ihn per -D. Ein fest auf 4g gesenkter Wert würde den Normalfall verlangsamen, um einen
Ausnahmefall zu bedienen.
Konsequenz für die Dokumentation, nicht für den Code: der Deckel ist richtig, aber er ist keine
Garantie. Wer nativ baut, baut ein Modul zur Zeit und prüft vorher docker stats. Das ist genau die
Ruhephasen-/Lock-Logik, die dots native-promote.mjs automatisiert — siehe Punkt 2, der damit seine
erste echte Gegenevidenz bekommen hat.
Was weiterhin NICHT belegt ist: kein Binary wurde je gestartet. Die Aussage aus Durchgang 1 gilt
unverändert und jetzt für alle drei Module. Beim platform-service hängt daran mehr als sonst: sein
Katalog-GET serialisiert ActorType aus zwirn-security-java-core, und ohne den (neu gesetzten)
Jandex-Index quarkus.index-dependency.zwirn-security-core.* fehlte im Image die
Reflection-Registrierung. Das ist genau die Fehlerklasse, die erst beim ersten Request zuschlägt —
ein gebautes Binary sagt darüber nichts.
2. Kein native-promote.mjs-Äquivalent — bewusst, aber die Begründung ist schwächer geworden. Dots
Skript automatisiert drei der fünf Mechanismen aus GG-DEVOPS-0003: Ruhephasen-Erkennung, ein Lock für
strikt sequenzielle Builds, Staleness-Schutz mit automatischem Rollback. Alle drei arbitrieren
zwischen 13 Modulen auf einer Maschine.
Die bisherige Begründung lautete: „dieses Produkt hat einen einzigen Data-Service — es gibt nichts zu arbitrieren, keinen zweiten Build, der RAM wegnimmt." Beides stimmt seit dem 2026-08-10 nicht mehr. Es sind drei Module, und der Beleg, dass zwei aufeinanderfolgende Builds sich gegenseitig aus dem Speicher drängen, liegt oben in Punkt 1b vor.
Trotzdem vorerst kein Skript: drei Module lassen sich von Hand nacheinander bauen, und die relevante
Konkurrenz kam nicht von diesem Produkt, sondern von drei fremden Compose-Stacks auf derselben
Maschine — dagegen hilft ein produktlokales Lock nicht. GG-DEVOPS-0003 deckt das ab: der
Automatisierungsgrad ist verhandelbar, das Artefakt nicht. Neu zu bewerten, sobald ein nativer
Bau Teil einer CI wird (dort ist „ein Modul zur Zeit" keine Disziplinfrage mehr, sondern Konfiguration).
3. Das UI hat kein auslieferbares Image. tuxametrics-ui hat weder Dockerfile noch nginx-Setup
noch einen Service im Compose-Stack — obwohl application.properties CORS für localhost:8121
freischaltet, einen Port, den niemand bedient. Das ist kein Native-Thema (ein React-Build ist immer
statisches JS), gehört aber in dieselbe Frage „was deployen wir eigentlich nicht-lokal".
Der bisherige Blocker ist weg: die IdP-Frage ist entschieden (Realm digital-labs, Client
tuxametrics-ui), und AuthShell liefert im gebauten Artefakt bereits OIDC — eine Runtime-Config über
window.__ENV__ (Zwirn-ADR-0031) ist damit sinnvoll befüllbar. Was fehlt, ist nur noch das Image
selbst plus ein Reverse-Proxy, der /realms und die /api/*-Pfade auf dieselbe Origin legt, wie es
der Vite-Dev-Proxy heute tut.
4. tuxametrics-process-hub-cockpit bleibt bewusst JVM — eine benannte Ausnahme. Flowable/Spring
stützt sich stark auf Reflection und dynamische Proxies; ein natives Image dafür ist ein eigenes Projekt
mit eigener Reachability-Konfiguration und laufender Pflege. Dieselbe Entscheidung wie bei
dkfz-prozesswerkstatt. Das steht so in docker-compose.yml, docker-compose.native.yml und
ADR-0005 — damit es eine Entscheidung bleibt
und kein Blindflug wird.