Ich wollte unseren Zwölf-Container-Stack auf vier kürzen. Zwei meiner drei Schlussfolgerungen waren falsch.
SOIT Team

Selbst gehostete Projekte verlieren die meisten Interessenten beim ersten Befehl in der README, und unserer nennt zwölf Dienste in einer einzigen Zeile. Ticket #25 stellte die naheliegende Frage: Ich will es mir doch nur ansehen, brauche ich wirklich alle? Die Antwort lautet nein. Sieben der zwölf genügen, und da drei davon einmalige Aufgaben sind, die sich selbst beenden, bleiben nur vier Container dauerhaft laufen: postgres, minio, api und web. Das Nützlichste an der Übung ist allerdings, dass der gekürzte Stack tatsächlich ausgeführt und nicht bloß auf dem Papier durchdacht wurde. Zwei der drei aus dem Code gelesenen Schlussfolgerungen erwiesen sich als falsch, und genau die beiden falschen hätten einen Erstnutzer vor einer toten URL sitzen lassen.
Der schnellste Weg herauszufinden, ob eine Abhängigkeit echt ist, ist nicht der Deployment-Leitfaden, sondern der Health-Check, denn ein Leitfaden dokumentiert Absicht, ein Health-Check dokumentiert Verhalten. Im Readiness-Endpunkt werden drei Backends unterschiedlich behandelt: Ist die Datenbank nicht erreichbar, wird ein 503 ausgelöst; ist der Objektspeicher nicht erreichbar, ebenfalls ein 503; ist der Vektorspeicher nicht erreichbar, steht im Feld schlicht nicht verfügbar, und der Endpunkt liefert weiterhin 200. Diese Asymmetrie benennt die harten Anforderungen unmissverständlich: ein erreichbares Postgres und ein beschreibbares Storage-Wurzelverzeichnis. Alles Übrige ist im Prinzip verhandelbar, und der Rest dieses Rundgangs handelt davon, welche dieser Verhandlungen den Kontakt mit dem veröffentlichten Image überstehen.
Storage-Wurzelverzeichnis ist nicht dasselbe Wort wie MinIO, und der Storage-Adapter löst tatsächlich auf eine lokale Datei-URI auf, wenn nichts anderes konfiguriert ist. Die erste Schlussfolgerung lautete deshalb, der Objektspeicher könne weg. Kann er nicht. Zeigte der Storage auf ein lokales Verzeichnis, startete die API mit der Meldung, der Objektspeicher sei nicht verfügbar, und der von Hand im Container gebaute Adapter zeigte den wahren Fehler: Zugriff verweigert auf einem Pfad, den niemand angegeben hatte. Ursache ist eine Normalisierungsfunktion, die Schrägstriche an beiden Enden des Wurzelpfads entfernt und damit einen absoluten Pfad in einen relativen verwandelt; dieser relative Pfad wird dann gegen das Arbeitsverzeichnis des Images aufgelöst, das im Dockerfile als root hineinkopiert wird, während der Prozess unprivilegiert läuft. Ein Volume zu mounten rettet es ebenfalls nicht, denn Docker legt auch den Mountpunkt als root an. MinIO bleibt also, was mit ein paar hundert Megabyte günstig genug ist, und der zugrunde liegende Fehler ist als Ticket #43 erfasst.
Die Vektorgruppe geht nur im Ganzen, und es lohnt sich, genau zu sagen, was das kostet. Milvus hängt für Metadaten an etcd und für Daten an MinIO, diese Namen reisen also gemeinsam. Die API startet auch ohne sie, weil die Vektorverbindung erst bei der ersten Nutzung träge aufgebaut wird und nicht während der Dependency Injection. Was sie nicht tut, ist auf leere Ergebnisse herunterzuschalten: Die In-Memory-Vektorimplementierung ist Testläufen vorbehalten und fragt, anders als der Secrets-Port, nie die Umgebungseinstellung ab. Das Ergebnis: Die Plattform startet, nicht vektorbezogene Funktionen laufen, Readiness meldet den Vektorspeicher ehrlich als nicht verfügbar und nennt sich dennoch bereit, und die Wissenssuche wirft einen Fehler, sobald man sie benutzt. Diese Schlussfolgerung hielt stand.
Die zweite falsche Schlussfolgerung ist jene, die kein noch so gründliches Lesen hervorgebracht hätte. Jene ehrliche Readiness-Antwort brauchte vierunddreißig Sekunden, weil die Vektorsonde einen Hostnamen auflösen muss, den es nicht mehr gibt, und nichts ihre Wartezeit begrenzt. Die Sonde wurde geschrieben, um weich zu scheitern, nicht um schnell zu scheitern. Compose prüft die Gesundheit der API derweil mit einer Drei-Sekunden-Anfrage und fünf Sekunden Nachsicht, sodass die Prüfung nicht bestehen kann und der Container dauerhaft als ungesund markiert bleibt, obwohl der Dienst dahinter einwandfrei arbeitet und Anmeldungen annimmt. Der Web-Container erklärt, dass er von einer gesunden API abhängt, weshalb bei einem normalen Start die Oberfläche gar nicht erst hochkommt. Die Abhilfe ist ein einziger Schalter, web unter Überspringen der Abhängigkeiten zu starten, und das fehlende Sonden-Timeout ist als Ticket #44 erfasst.
Drei Dienste sind wirklich optional, jeder aus einem anderen Grund. Vault darf ausbleiben: Lässt man URL und Token leer, schaltet die Verdrahtung auf einen prozessinternen Secret-Store um, was sie in Entwicklungsumgebungen erlaubt, zu dem Preis, den ihr Name schon nennt, nämlich dass Secrets einen Neustart nicht überleben. Redis ist nicht eine Frage, sondern drei, mit unterschiedlichen Antworten: Der Event-Bus lässt sich auf seine In-Memory-Voreinstellung zurückschalten, die nur innerhalb eines einzelnen Prozesses zustellt; der Berechtigungs-Cache degradiert sauber, weil ein unerreichbares Redis als Cache-Miss gelesen wird und der Aufrufer erneut die Datenbank befragt; und der Rate-Limiter hat überhaupt kein In-Memory-Gegenstück, wird aber nur aufgerufen, wenn ein Limit pro Werkzeug oder ein Tageskontingent konfiguriert ist. Redis aus einer Demo zu streichen ist deshalb sicher, solange man diese beiden nicht konfiguriert, der einzige Punkt dieser Liste, der davon abhängt, was man vorführen möchte.
Ein Dienst wird nicht entfernt, sondern eingefaltet. Der Outbox-Dispatcher betreibt die transaktionale Outbox, und seine Einstellung bestimmt, wo diese Arbeit geschieht, nicht ob sie geschieht: Compose deaktiviert den prozessinternen Weg und lässt dieselbe Logik in einem eigenen Container laufen, und eine einzige Umgebungsvariable dreht das für eine Demo um. Die Produktion geht den umgekehrten Weg und erzwingt ihn im Code, statt ihn in der Dokumentation zu empfehlen, denn Zustellung und Anfragebearbeitung in einem Prozess konkurrieren um dieselben Ressourcen, und ein Neustart unterbricht beides zugleich. Der Knowledge-Ingest-Worker schließlich hat genau eine Aufgabe und keinen Grund zu existieren, wenn die Demo nie ein Dokument hochlädt; und wo wir dabei sind: Die schweren Machine-Learning-Abhängigkeiten, die viele in diesem Image vermuten, liegen tatsächlich in einer anderen optionalen Gruppe.
Nichts davon ist eine unterstützte Deployment-Form, und es lohnt sich zu sagen, warum die Voreinstellung zwölf verlangt. Jeder hier gestrichene Dienst entspricht etwas, das die Produktion beim Start erzwingt: Die Laufzeitprüfung verweigert den Start ohne den Redis-Event-Bus, ohne den eigenen Dispatcher-Prozess, ohne einen echten Secret-Manager, ohne Telemetrie und ohne Signaturprüfung für Plugins. Die Standardtopologie zielt auf die Produktionsform, nicht auf die Demoform, und der Abstand zwischen beiden erweist sich am Ende als messbar in einer Handvoll Umgebungsvariablen, was zufällig der schnellste Weg ist, diese Architektur zu verstehen. Behalten Sie auch die andere Lehre: Den Code zu lesen liefert Hypothesen, ihn auszuführen liefert Schlussfolgerungen. Das Repository liegt auf github.com/soit-ai/soit, der vollständige Quickstart in docs/quickstart.md, und wer bei einem Schritt der gekürzten Variante hängenbleibt, sagt es am besten im Issue-Tracker.