Eure Dokumente, euer Cluster: einen kompletten RAG-Stack mit Docker und Helm deployen
Ein RAG-System, das Fragen zu internen Dokumenten beantwortet, sollte diese Dokumente nirgendwohin schicken müssen. Teil 4 macht den Offline-Stack zu einem helm install.
Teil 4 der RAG-Tutorial-Serie verpackt den kompletten Stack — RAG-Loop, austauschbare Anbieter, Chat-Konnektoren — für Produktion: ein Multi-Stage-Dockerfile für das Spring-Boot-Backend, ein nginx-Image für das Angular-Frontend, das API- und Webhook-Pfade reverse-proxied, und ein Helm-Chart mit Backend-/Frontend-Deployments, ConfigMap für nicht-geheime Env-Vars, Secret für API-Keys und Webhook-Secrets, optionalem Ingress und optionalem In-Cluster-Postgres+pgvector und Ollama. Dieselben Spring-Profil-Strings, die auf dem Laptop Anbieter und Speicher wählen, wählen sie im Cluster — inklusive der komplett offline laufenden Kombination: lokales Mistral, eigene Datenbank, kein API-Key, kein Dokument verlässt den Cluster.
Ein RAG-System beantwortet Fragen zu euren internen Dokumenten. Lies den Satz nochmal, bevor du Infrastruktur auswählst: Was auch immer du deployst, wird eure Verträge, euer Wiki, eure unveröffentlichten Pläne halten — als Klartext-Chunks und als Embeddings. Teil 4, der letzte Teil meiner RAG-Tutorial-Serie, verpackt alles aus Teil 1–3 für Produktion — und seine wichtigste Eigenschaft ist, dass die komplett self-hosted Variante, bei der kein Dokument den Cluster verlässt, ein helm install entfernt ist. Kein Spezial-Build.
Dieselben Profile, vom Laptop bis in den Cluster
Der Anwendungscode ist unverändert gegenüber Teil 3 — mit Absicht. Teil 4 ergänzt Verpackung: ein Multi-Stage-Dockerfile für das Spring-Boot-Backend, ein Angular-Build-in-nginx-Image für das Frontend und ein Helm-Chart.
Die Design-Entscheidung, die ich am härtesten verteidigen würde: Das Chart erfindet keine neue Konfigurationssprache. backend.springProfiles: "pgvector,ollama" ist derselbe String, den du lokal an mvn spring-boot:run übergibst. Die Profil-Mechanik aus Teil 2 — Anbieter × Speicher, alles Weitere abgeleitet — trägt unübersetzt vom Laptop in den Cluster. Jede Mapping-Schicht zwischen „wie ich es lokal starte" und „wie es in Prod läuft" ist eine Stelle, an der beides leise auseinanderdriftet; das Chart hat schlicht keine.
Drei Install-Beispiele liegen beim Chart: in-memory + Azure (kleinster Fussabdruck), pgvector + OpenAI (persistenter Speicher) — und das, auf das es im Datensouveränitäts-Gespräch ankommt:
helm install rag ./helm/rag-tutorial \
--set backend.springProfiles="pgvector\,ollama" \
--set postgres.enabled=true \
--set postgres.password="$(openssl rand -hex 16)" \
--set ollama.enabled=true
Lokales Mistral, eigenes Postgres mit pgvector, nirgends ein API-Key in den Values. Steht postgres.enabled oder ollama.enabled auf true, bekommt das Backend POSTGRES_URL und OLLAMA_BASE_URL automatisch auf die Cluster-Services gezeigt — eine Abhängigkeit einschalten und sie verdrahten ist ein Flag, nicht zwei Schritte, die sich widersprechen können.
Config und Secrets: nach Sensibilität getrennt, leer heisst weggelassen
Das Chart trennt die Backend-Umgebung in eine ConfigMap (Endpoints, Modellnamen, Confluence-Base-URL) und ein Secret (API-Keys, Teams-HMAC-Secret, Slack-Signing-Secret und Bot-Token), beides per envFrom eingebunden, beides standardmässig leer, echte Werte beim Installieren per --set — nie eingecheckt.
Ein Template-Detail verdient seinen Kommentar: Einträge mit leeren Werten werden übersprungen, nicht gerendert. Das ist keine Kosmetik. Die Konnektoren aus Teil 3 aktivieren sich über das Vorhandensein einer Property — ein leeres RAG_SLACK_SIGNING_SECRET="" in der Umgebung würde als „gesetzt" zählen und einen Slack-Konnektor ohne Secret hochfahren. Ein Template, das leere Strings rendert, würde die Feature-Flags still aushebeln. Deployment-Maschinerie muss die Aktivierungssemantik der Anwendung verstehen, sonst arbeitet sie gegen sie.
Zweites Detail: Die Deployments tragen Checksummen von ConfigMap und Secret als Pod-Annotations, damit ein helm upgrade mit geänderter Konfiguration die Pods rollt. Ohne das landet die Änderung im Cluster, aber nicht in den laufenden Prozessen — die klassische „ich hab's doch geändert, warum ist es noch kaputt"-Stunde.
nginx löst nebenbei das Webhook-Problem
Das Frontend-Image liefert das Angular-Dashboard aus und reverse-proxied drei Pfad-Präfixe zum Backend: /api, /teams, /slack. Das dritte Paar ist die Belohnung: Während der lokalen Entwicklung von Teil 3 erreichten Teams und Slack das Backend nur durch einen ngrok-Tunnel, weil beide Plattformen auf öffentlichen HTTPS-Endpunkten bestehen. Deployed hinter einem Host mit Ingress sind die Webhook-URLs einfach da — stabil, öffentlich, auf derselben Domain wie das Dashboard. Ein Deployment-Anliegen (ein öffentlicher Eingang) hat ein Integrations-Anliegen (Tunnel) als Nebeneffekt aufgelöst.
Verifiziert, nicht angenommen
Die Gewohnheit, die in dieser Serie echte Bugs gefunden hat, gilt auch fürs Verpacken: Beide Images bauen aus sauberem Kontext, das Chart besteht helm lint, und helm template rendert sowohl die Default- als auch die Alles-aktiviert-Konfiguration. Der Startup-Crash aus Teil 2 — Azure-Autokonfiguration aktiviert sich im Offline-Profil — wurde gefunden, indem das Artefakt ohne Zugangsdaten gebootet wurde. Genau das tut ein frischer Cluster mit deinem Pod. Deine CI sollte „rendert und bootet ohne Credentials" als Test behandeln, nicht als Hoffnung.
Der Takeaway — und die Serie
Wenn dein RAG-System existiert, weil deine Dokumente sensibel sind, darf Self-Hosting kein Nachgedanke sein — mach die Offline-Variante zu einem erstklassigen Install-Ziel und halte lokale und Cluster-Konfiguration in denselben Worten. An diesem Standard darfst du jedes interne Wissenssystem messen, ob gebaut oder gekauft.
Die Serie, komplett: Teil 1 — der kleinste funktionierende RAG-Loop; Teil 2 — austauschbare LLMs und Speicher per Profil; Teil 3 — Confluence, Teams und Slack als standardmässig deaktivierte Konnektoren; Teil 4 — dieser Post. Jeder Teil ist ein eigenständig lauffähiges Projekt mit Walkthrough auf Deutsch und Englisch: github.com/halviclabs/rag-tutorials.
Und wenn du eigentlich Agenten willst, die ein Ticket bis zu getestetem, reviewtem, deploytem Code durchziehen — das ist dieselbe Engineering-Disziplin in grösserem Massstab, und genau das bauen wir bei anvil-coder.
Häufige Fragen
Wie deploye ich eine Spring-AI-RAG-Anwendung nach Kubernetes?
Zwei Images bauen — ein Multi-Stage-Maven-Build für das Spring-Boot-Backend und ein Angular-Build-in-nginx-Image für das Frontend —, in eine Registry pushen und das Helm-Chart installieren. Das Chart verdrahtet Profile und Zugangsdaten über Values: backend.springProfiles wählt Anbieter und Speicher exakt wie auf dem Laptop, nicht-geheime Konfiguration landet in einer ConfigMap, Secrets werden beim Installieren per --set übergeben und landen in einem Kubernetes-Secret.
Kann ein RAG-Stack komplett offline und self-hosted auf Kubernetes laufen?
Ja — das ist das dritte Install-Beispiel des Charts: postgres.enabled=true und ollama.enabled=true deployen pgvector und Ollama im Cluster, das Backend bekommt POSTGRES_URL und OLLAMA_BASE_URL automatisch darauf gezeigt, und die Profile pgvector,ollama wählen lokales Mistral und nomic-embed-text. Kein API-Key, kein Cloud-Aufruf, kein Dokument verlässt den Cluster. Beim ersten Start mehrere GB Modell-Download ins Ollama-PVC einplanen und dem Pod echte CPU/Memory oder einen GPU-Node geben.
Wie gehören Secrets in ein Helm-Chart?
Konfiguration nach Sensibilität trennen: nicht-geheime Env-Vars (Endpoints, Modellnamen) in eine ConfigMap, API-Keys und Webhook-Secrets in ein Secret, beides per envFrom eingebunden. Values sind standardmässig leer und werden übersprungen, wenn nichts gesetzt ist — ein leeres Secret würde sonst als "Property vorhanden" zählen und feature-geflaggte Konnektoren versehentlich aktivieren. Echte Werte beim Installieren per --set übergeben; nie in values.yaml einchecken.
Warum hilft das Deployment speziell den Teams- und Slack-Konnektoren?
Beide Plattformen rufen nur öffentliche HTTPS-Endpunkte auf — lokal erzwingt das ngrok-Tunnel. Sobald das nginx des Frontends (oder der Ingress) einen öffentlichen Host bedient und /teams und /slack zum Backend proxied, bekommen die Webhooks stabile öffentliche URLs als Nebeneffekt des Deployments — kein Tunnel, keine URL, die bei jedem Neustart wechselt.