Brainversum · tuxametrics Graph Admin

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ünGG-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):

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:

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"):

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.