Inhaltsverzeichnis:
RAG-Architektur mit Streamlit, Qdrant und Ollama
Die Docker-Architektur trennt die RAG-Anwendung in drei klar abgegrenzte Dienste: Die Streamlit-Oberfläche nimmt Fragen entgegen, Qdrant liefert passende Textabschnitte und der Ollama-Dienst erzeugt daraus die Antwort. Diese Trennung macht Fehler leichter auffindbar und erlaubt es, einzelne Komponenten später auszutauschen.
Der Ablauf beginnt mit der Eingabe des Nutzers. Die Anwendung wandelt die Frage in einen Vektor um und sucht in Qdrant nach ähnlichen Dokumentabschnitten. Nur die relevantesten Treffer gelangen in den Prompt des Sprachmodells. Das Modell antwortet damit nicht aus einem unbeschränkten Datenbestand, sondern auf Basis eines kontrollierten Kontexts. Genau hier liegt der praktische Vorteil der RAG-Architektur.
- Streamlit: verarbeitet Eingaben und zeigt Antworten an.
- Qdrant: sucht nach ähnlichen Embeddings und liefert Dokumenttreffer.
- Ollama: verarbeitet Frage und Kontext mit einem lokal bereitgestellten Sprachmodell.
Innerhalb des Compose-Netzwerks sprechen die Container über ihre Servicenamen miteinander. Die Anwendung verwendet daher nicht localhost für Datenbank oder Modellserver. In einem Container würde localhost nämlich auf denselben Container zeigen. Für die interne Kommunikation sind Adressen wie http://qdrant:6333 und http://ollama:11434 entscheidend.
Ein wichtiger Architekturpunkt wird oft übersehen: depends_on startet Dienste zwar in einer festgelegten Reihenfolge, prüft aber nicht automatisch, ob Qdrant oder der Modellserver bereits vollständig bereit sind. Die Anwendung sollte deshalb Verbindungsfehler abfangen und Wiederholungen mit kurzer Wartezeit ausführen. Sonst erscheint die Oberfläche, während die erste Anfrage noch scheitert. Das wirkt dann wie ein RAG-Fehler, obwohl nur der Dienststart nicht abgeschlossen war.
Für belastbare Antworten müssen zwei Datenpfade getrennt bleiben. Beim Erstellen der Wissensbasis entstehen Embeddings. Beim Fragenlauf wird die Nutzereingabe mit demselben Embedding-Modell umgewandelt. Werden später unterschiedliche Modelle oder Dimensionen verwendet, passen die Vektoren nicht mehr zusammen. Ein Wechsel erfordert daher meist eine neue Collection und eine vollständige Indexierung.
Die Ressourcenplanung richtet sich vor allem nach dem Sprachmodell. Ein Modell mit sieben bis acht Milliarden Parametern benötigt je nach Quantisierung grob mehrere Gigabyte Arbeitsspeicher. Größere Modelle erhöhen Antwortqualität und Latenz, verlangen aber oft deutlich mehr RAM oder GPU-Speicher. Qdrant bleibt bei kleinen bis mittleren Wissensbeständen meist vergleichsweise genügsam. Der Engpass sitzt also nicht automatisch dort, wo die meisten Daten liegen.
Für den produktiven Betrieb sollten interne Ports nur im Compose-Netzwerk verfügbar sein. Nach außen genügt meist ein einziger Zugang zur Webanwendung. Zusätzlich gehören Gesundheitsprüfungen, beschränkte Containerrechte, feste Image-Versionen und eine klare Protokollierung in die Konfiguration.
Beispielprojekt Winy klonen und Docker-Struktur prüfen
Das Beispielprojekt lässt sich direkt aus dem öffentlichen Repository beziehen. Führen Sie den Klon im gewünschten Arbeitsverzeichnis aus:
git clone https://github.com/mfranzon/winy.git
Wechseln Sie anschließend in den Projektordner und prüfen Sie zunächst die Dateien, bevor Sie Container bauen:
cd winy
Die Struktur trennt Anwendungscode, Hilfsprogramme und Orchestrierung. Besonders wichtig sind dabei die folgenden Bereiche:
- app/: enthält die Streamlit-Anwendung, ihr Dockerfile und die Python-Abhängigkeiten.
- tools/: bündelt Skripte für Datenbank, Embeddings, Tests und den Modellbezug.
- docker-compose.yaml: beschreibt die gemeinsam gestarteten Dienste.
- wine_database.db: stellt die lokale Ausgangsdatenbank des Beispiels bereit.
- .gitignore: verhindert, dass lokale Artefakte versehentlich in die Versionsverwaltung gelangen.
Öffnen Sie danach die Compose-Datei und kontrollieren Sie, ob die dort verwendeten Build-Pfade tatsächlich existieren. Der Eintrag context: ./app bedeutet, dass Docker nur den Inhalt des App-Verzeichnisses als Build-Kontext erhält. Benötigt der Dockerfile Dateien aus dem Hauptordner, schlägt der Build fehl. In diesem Fall muss entweder der Kontext angepasst oder die benötigte Datei in den App-Ordner verschoben werden.
Prüfen Sie außerdem die beiden Abhängigkeitsdateien. Die Anwendung und die Werkzeuge können unterschiedliche Python-Pakete benötigen. Ein sauberer Aufbau installiert nur die Bibliotheken im jeweiligen Container, statt Entwicklungs- und Laufzeitpakete ungefiltert zu vermischen. Das verkleinert Images und reduziert mögliche Versionskonflikte.
Vor dem ersten Start lohnt sich ein Blick in main.py. Suchen Sie dort nach Umgebungsvariablen, festen Dateinamen und erwarteten Collection-Namen. Stimmen diese Werte nicht mit der Compose-Konfiguration oder der vorhandenen Datenbasis überein, startet der Container zwar, liefert aber keine brauchbaren Treffer.
Auch das Repository sollte vor der Nutzung auf lokale Änderungen geprüft werden:
git status
Für reproduzierbare Builds empfiehlt sich außerdem ein fester Commit oder ein geprüfter Versionsstand. Ein späteres git pull kann Quellcode, Abhängigkeiten und Konfigurationsannahmen verändern. Dokumentieren Sie diesen Stand in der Projektdokumentation, besonders wenn mehrere Personen am RAG-System arbeiten.
Geheime Zugangsdaten gehören nicht in docker-compose.yaml und keinesfalls in das Repository. Nutzen Sie dafür eine lokale .env-Datei, eine nicht versionierte Konfigurationsdatei oder die Secret-Verwaltung der Zielumgebung. Die Beispielanwendung kann lokal ohne solche Angaben auskommen; bei einem externen Modellserver ändert sich diese Anforderung jedoch schnell.
Komponenten, Aufgaben und Vorteile des Docker-basierten RAG-Systems
| Komponente | Aufgabe | Wichtige Konfiguration | Vorteil |
|---|---|---|---|
| Streamlit | Nimmt Fragen entgegen und zeigt die generierten Antworten an. | Port 8501, Bindung an 0.0.0.0 | Einfache webbasierte Benutzeroberfläche |
| Qdrant | Sucht anhand von Embeddings nach relevanten Dokumentabschnitten. | Interne Adresse http://qdrant:6333, persistentes Volume | Schnelle und strukturierte Vektorsuche |
| Ollama | Erzeugt Antworten aus Frage und bereitgestelltem Kontext. | Interne Adresse http://ollama:11434, Modell-Volume | Lokale Verarbeitung und flexible Modellwahl |
| Docker Compose | Startet und verbindet die einzelnen Dienste. | Gemeinsames Bridge-Netzwerk und Umgebungsvariablen | Reproduzierbare und übersichtliche Bereitstellung |
| Persistente Volumes | Speichern Qdrant-Daten, Embeddings und Ollama-Modelle dauerhaft. | Volumes für /qdrant/storage und /root/.ollama | Daten bleiben nach Container-Neustarts erhalten |
| Healthchecks und Wiederholungen | Prüfen die Dienstbereitschaft und fangen temporäre Verbindungsfehler ab. | HTTP-Healthchecks, Timeouts und begrenzte Retries | Zuverlässiger Start und weniger Fehlermeldungen beim ersten Zugriff |
Dockerfile für die Streamlit-Anwendung einrichten
Der Dockerfile der Streamlit-Anwendung baut ein eigenständiges Laufzeit-Image. Er installiert nur die für die Oberfläche benötigten Pakete, kopiert den Anwendungscode und startet Streamlit mit einem klar definierten Befehl.
Ein passender Dockerfile kann so aussehen:
FROM python:3.11-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
EXPOSE 8501
CMD ["streamlit", "run", "main.py", "--server.address=0.0.0.0", "--server.port=8501"]
Das schlanke Python-Image enthält weniger Systemkomponenten als ein vollständiges Entwicklungs-Image. Benötigt ein Paket native Bibliotheken, können zusätzliche Debian-Pakete erforderlich sein. Installieren Sie dann nur die tatsächlich verwendeten Werkzeuge und löschen Sie anschließend den Paket-Cache.
Die Reihenfolge der COPY-Befehle verbessert die Wiederverwendung des Docker-Caches: Zuerst wird die Datei mit den Abhängigkeiten kopiert und installiert, erst danach folgt der Quellcode. Ändert sich nur main.py, kann Docker die bereits gebaute Abhängigkeitsschicht wiederverwenden.
In requirements.txt gehören ausschließlich Pakete, die der Webdienst zur Laufzeit importiert. Versionsbereiche sollten bewusst gewählt werden. Eine vollständig offene Angabe wie package>=1.0 kann Monate später zu einem anderen Ergebnis führen. Für reproduzierbare Images sind geprüfte Versionen oder eine eingefrorene Lock-Datei sinnvoll.
- WORKDIR: legt den einheitlichen Arbeitsordner im Container fest.
- ENV: verhindert Python-Bytecode und sorgt für sofort sichtbare Logs.
- EXPOSE: dokumentiert den vorgesehenen Container-Port, veröffentlicht ihn aber nicht selbst.
- CMD: definiert den normalen Start der Streamlit-Anwendung.
Streamlit muss auf 0.0.0.0 lauschen. Die Bindung an 127.0.0.1 wäre innerhalb des Containers zwar technisch möglich, von außen aber nicht erreichbar. Der Port in EXPOSE und der Port im Startbefehl sollten übereinstimmen.
Entwicklungsdateien, lokale Datenbanken, virtuelle Umgebungen und Cache-Verzeichnisse gehören in eine .dockerignore. Ein typischer Inhalt umfasst __pycache__, .git, .venv, .env und temporäre Testdateien.
Für die Sicherheit läuft der Prozess idealerweise nicht als Root. Dafür wird im Dockerfile ein eigener Benutzer angelegt, der nur auf die erforderlichen Dateien zugreifen darf. Für einen dauerhaft betriebenen Dienst ist dieser zusätzliche Schritt empfehlenswert.
Der Dockerfile sollte keine Modellgewichte oder Embedding-Dateien einbacken, sofern diese separat verwaltet werden können. Der Container bleibt dann für Code zuständig, während Daten und Modelle über geeignete Volumes oder externe Speicher bereitgestellt werden.
Docker-Compose-Netzwerk für alle Dienste konfigurieren
Das Compose-Netzwerk verbindet die drei Container über ein internes, automatisch aufgelöstes Netzwerk. Eine robuste Konfiguration kann so aussehen:
services:
winy:
build:
context: ./app
environment:
QDRANT_CLIENT: http://qdrant:6333
OLLAMA: http://ollama:11434
ports:
- "8501:8501"
networks:
- rag_net
qdrant:
image: qdrant/qdrant:latest
networks:
- rag_net
ollama:
image: ollama/ollama:latest
networks:
- rag_net
networks:
rag_net:
driver: bridge
qdrant und ollama sind die Namen der Compose-Dienste. Der integrierte DNS-Dienst übersetzt sie automatisch in die jeweilige Container-IP. Feste IP-Adressen sind deshalb unnötig.
Nur der Webdienst benötigt in diesem Szenario eine veröffentlichte Schnittstelle. Die Ports der internen Dienste können aus der Compose-Datei entfernt werden, sofern kein Zugriff vom Hostsystem erforderlich ist. Für eine Diagnose lässt sich der Datenbank-Port vorübergehend veröffentlichen; dauerhaft sollte dieser Zugriff nicht offen bleiben.
Mehrere Netzwerke bieten zusätzliche Trennung. Ein Frontend-Netzwerk kann die Streamlit-Anwendung mit einem Reverse-Proxy verbinden, während ein separates Backend-Netzwerk nur den Zugriff auf die Vektorsuche und den Modellserver erlaubt. Für eine lokale Installation ist ein einziges Bridge-Netzwerk meist ausreichend.
Bei der Namensgebung zählt Konsistenz. Ändert der Dienst beispielsweise von qdrant zu vector-db, muss auch die Verbindungsvariable angepasst werden. Für die interne Kommunikation sollte bevorzugt der Servicename verwendet werden.
Ein Netzwerk kann mit internal: true vom externen Zugriff isoliert werden. Diese Option eignet sich für ein reines Backend-Netzwerk. Der Webdienst braucht dann zusätzlich ein normales Netzwerk für den eingehenden Zugriff. Eine zu strenge Isolation kann Updates, Modellabrufe oder externe Schnittstellen blockieren.
- Interne Kommunikation: Servicename plus Container-Port verwenden.
- Externer Zugriff: nur benötigte Schnittstellen veröffentlichen.
- Netzwerkdesign: bei kleinen Installationen ein Netz, bei größeren Setups getrennte Zonen.
- Konfiguration: Hostnamen zentral über Umgebungsvariablen pflegen.
Für die Prüfung der Namensauflösung hilft ein temporärer Testcontainer im selben Netzwerk. Mit docker compose exec winy getent hosts qdrant lässt sich feststellen, ob der Dienstname aufgelöst wird. Ein erfolgreicher DNS-Eintrag beweist allerdings noch nicht, dass der Dienst Anfragen annimmt. Dafür ist zusätzlich ein Health-Endpunkt oder ein einfacher HTTP-Test nötig.
Qdrant mit persistentem Volume als Vektordatenbank betreiben
Ein persistentes Volume schützt die Qdrant-Daten vor dem Entfernen oder Neuerstellen des Containers. Ohne diese Zuordnung liegen Collection, Index und Metadaten nur in der beschreibbaren Container-Schicht. Wird der Container gelöscht, ist die Vektorbasis weg.
In der Compose-Datei wird der Speicher am Dienst und im globalen Bereich definiert:
services:
qdrant:
image: qdrant/qdrant:latest
volumes:
- qdrant_data:/qdrant/storage
volumes:
qdrant_data:
Der linke Teil bezeichnet das Docker-Volume, der rechte den Speicherpfad innerhalb des Qdrant-Containers. Docker verwaltet den dauerhaften Speicher, Qdrant erwartet seine Daten unter /qdrant/storage. Ein vertauschtes Ziel führt meist zu einer leeren Datenbank nach dem Neustart.
Ein benanntes Volume eignet sich für lokale Entwicklung und einfache Einzelserver. Ein Bind-Mount bietet dagegen direkten Zugriff auf einen Hostordner:
volumes:
- ./data/qdrant:/qdrant/storage
Diese Variante erleichtert manuelle Sicherungen und Dateikontrollen, verlangt aber passende Dateirechte und eine saubere Ordnerverwaltung. Für ein möglichst portables Projekt ist ein benanntes Volume oft die ruhigere Wahl.
Prüfen Sie den Speicher nach dem Start mit:
docker volume ls
docker volume inspect _qdrant_data
Compose versieht das Volume häufig mit dem Projektnamen. Wer einen festen Namen benötigt, kann ihn ausdrücklich vergeben:
volumes:
qdrant_data:
name: rag-qdrant-data
Zwei Qdrant-Instanzen dürfen nicht gleichzeitig unkoordiniert auf dieselben Storage-Dateien schreiben.
Ein Volume ist kein Backup. Es schützt vor dem Löschen eines Containers, nicht vor defekten Dateien, Fehlbedienung, Ransomware oder einem Ausfall des Hosts. Für eine belastbare Sicherung muss Qdrant angehalten oder ein konsistenter Snapshot erstellt werden. Kopieren Sie nicht einfach während laufender Schreibvorgänge einzelne Indexdateien.
- Benanntes Volume: unkompliziert und gut für lokale Einzelinstanzen.
- Bind-Mount: direkter Hostzugriff, aber mehr Verantwortung bei Rechten und Pfaden.
- Snapshot: geeignet für konsistente Wiederherstellung.
- Volume-Prüfung: nach einem Neustart Collections und Trefferzahl kontrollieren.
Embeddings, Payloads und Indexstrukturen wachsen gemeinsam. Bei großen Dokumentmengen kann die Vektorbasis deutlich mehr Platz als die ursprünglichen Textdateien beanspruchen. Überwachen Sie deshalb freien Speicher und legen Sie eine Obergrenze für Uploads fest.
Ollama-Container starten und ein Sprachmodell laden
Der Modellserver benötigt nach dem Containerstart ein dauerhaftes Volume für seine Modellbestände. Ergänzen Sie den Dienst in der Compose-Datei beispielsweise so:
ollama:
image: ollama/ollama:latest
volumes:
- ollama_models:/root/.ollama
environment:
- OLLAMA_HOST=0.0.0.0:11434
volumes:
ollama_models:
Ohne diese Zuordnung verschwinden heruntergeladene Modelle beim Neuerstellen des Containers. Planen Sie ausreichend Speicher ein: Ein quantisiertes Modell mit etwa sieben Milliarden Parametern benötigt oft mehrere Gigabyte, größere Varianten deutlich mehr.
Starten Sie zunächst nur den Modellserver:
docker compose up -d ollama
Danach laden Sie ein passendes Modell in den laufenden Container:
docker compose exec ollama ollama pull llama3.2
Der konkrete Modellname ist austauschbar. Entscheidend ist, dass er exakt mit der späteren Anwendungskonfiguration übereinstimmt. Prüfen Sie die verfügbaren Modelle anschließend mit:
docker compose exec ollama ollama list
Ein kurzer Funktionstest zeigt, ob der Server das Modell tatsächlich verwenden kann:
docker compose exec ollama ollama run llama3.2 "Antworte mit einem kurzen Satz."
Für die RAG-Anwendung sollten Sie auch ein Embedding-Modell einplanen. Die Dokumentvektoren und die Suchanfrage müssen mit demselben Embedding-Modell erzeugt werden. Ein Wechsel nach der Indexierung macht in der Regel eine neue Vektorisierung erforderlich.
Die Modellwahl beeinflusst Antwortqualität, Speicherbedarf und Geschwindigkeit. Für einen lokalen Prototypen ist ein kompaktes, quantisiertes Modell meist der pragmatische Start; testen Sie danach mit echten Fragen statt nur mit einem Demo-Satz.
Auf einem Linux-Rechner mit kompatibler NVIDIA-GPU kann der Dienst GPU-Unterstützung erhalten. Dafür müssen die NVIDIA-Container-Laufzeit und passende Docker-Treiber eingerichtet sein. Eine mögliche Compose-Ergänzung lautet:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Ob diese Einstellung greift, hängt von Docker-Version, Betriebssystem und Laufzeit ab. Ohne GPU bleibt der Dienst nutzbar, antwortet dann jedoch häufig langsamer. Kontrollieren Sie die Ressourcen während eines Testlaufs und begrenzen Sie parallele Anfragen, wenn der Arbeitsspeicher knapp wird.
- Modell dauerhaft speichern: Volume auf /root/.ollama binden.
- Modell laden: ollama pull im laufenden Container ausführen.
- Modell prüfen: mit ollama list und einer kurzen Testanfrage.
- Embeddings abstimmen: Indexierung und Abfrage mit demselben Modell durchführen.
- Hardware messen: Antwortzeit, RAM- und GPU-Auslastung unter echter Last beobachten.
Lokale oder externe Ollama-Dienste per Umgebungsvariable anbinden
Die Adresse des Modellservers sollte nicht fest im Python-Code stehen. Eine Umgebungsvariable erlaubt, zwischen einem Container, einem Dienst auf dem Docker-Host und einem entfernten Server zu wechseln, ohne das Image neu zu bauen. Für die Anwendung genügt beispielsweise:
OLLAMA=http://ollama:11434
Der Hostname ollama funktioniert nur innerhalb des Compose-Netzwerks. Läuft der Modellserver dagegen auf dem Docker-Host, braucht die Anwendung je nach Betriebssystem eine spezielle Host-Adresse. Unter Docker Desktop ist häufig host.docker.internal geeignet:
OLLAMA=http://host.docker.internal:11434
Unter Linux muss diese Namensauflösung gegebenenfalls ausdrücklich ergänzt werden:
extra_hosts:
- "host.docker.internal:host-gateway"
Ein externer Server wird über seinen erreichbaren DNS-Namen oder seine IP-Adresse eingetragen:
OLLAMA=https://llm.example.net
Verwenden Sie dabei die Adresse aus Sicht des App-Containers. localhost ist fast nie die richtige Wahl, wenn der Modellserver außerhalb dieses Containers läuft.
Die Compose-Datei kann die Variable aus einer lokalen Konfigurationsdatei beziehen:
environment:
OLLAMA: ${OLLAMA_URL}
.env:
OLLAMA_URL=http://ollama:11434
Für einen Wechsel auf einen externen Dienst genügt dann eine Änderung in .env. In einer automatisierten Umgebung sollten Werte über die Secret- oder Konfigurationsverwaltung der Plattform gesetzt werden. Zugangstoken gehören nicht in Git, in Fehlermeldungen oder in die Browseroberfläche.
Ein häufiger Stolperstein ist der Parametername. Die Anwendung muss exakt die Variable lesen, die Compose setzt. Heißt der Code etwa OLLAMA_URL, während die Datei OLLAMA definiert, bleibt der erwartete Wert leer. Prüfen Sie die effektive Konfiguration mit:
docker compose config
Testen Sie danach die Verbindung aus dem App-Container. Je nach Image steht curl nicht zur Verfügung. Ein kleiner Python-Test funktioniert meist zuverlässiger:
docker compose exec winy python -c "import os, urllib.request; print(urllib.request.urlopen(os.environ['OLLAMA'], timeout=5).status)"
Bei einer entfernten Anbindung müssen zusätzlich TLS-Zertifikate, Firewall-Regeln und Proxy-Einstellungen stimmen. Ein selbst signiertes Zertifikat sollte nicht einfach durch das Abschalten der Prüfung umgangen werden. Besser ist es, die zuständige Zertifizierungsstelle im Container vertrauenswürdig zu hinterlegen.
- Container-intern: Servicename und interner Port verwenden.
- Docker-Host: eine vom Betriebssystem unterstützte Host-Gateway-Adresse einsetzen.
- Extern: DNS-Namen, Route, TLS und Firewall aus Sicht des Containers prüfen.
- Konfigurationswechsel: Variable über .env oder eine sichere Laufzeitkonfiguration setzen.
- Fehlersuche: effektive Compose-Werte und die Verbindung aus dem App-Container testen.
Bei wechselnden Modellservern sollte auch der Modellname separat konfiguriert werden:
OLLAMA_URL=http://ollama:11434
OLLAMA_MODEL=llama3.2
Die Anwendung kann beim Start prüfen, ob beide Werte vorhanden sind und ob der gewählte Modellname erreichbar ist. Eine verständliche Fehlermeldung ist hilfreicher als ein späterer Fehler mitten in der Anfrageverarbeitung.
RAG-Anwendung mit Docker Compose bauen und starten
Wechseln Sie in das Projektverzeichnis und lassen Sie Docker Compose die Anwendung aus der aktuellen Konfiguration bauen:
docker compose up --build
Der Parameter --build erzwingt die Neuerstellung des App-Images. Das ist besonders nach Änderungen an main.py, requirements.txt oder am Dockerfile sinnvoll. Für einen unveränderten Stand genügt später:
docker compose up -d
Die laufenden Container und ihre Zustände prüfen Sie mit:
docker compose ps
Direkt nach dem Start sollten Sie die Protokolle der Anwendung beobachten:
docker compose logs -f winy
Ein erfolgreicher Start zeigt, dass Streamlit auf dem vorgesehenen Port lauscht. Fehler beim Import, fehlende Umgebungsvariablen oder nicht erreichbare Abhängigkeiten erscheinen hier meist früher als in der Benutzeroberfläche.
Für einen sauberen Neuaufbau verwenden Sie:
docker compose down
docker compose build --no-cache
docker compose up -d
--no-cache ist nützlich, wenn eine veraltete Abhängigkeitsschicht den Build beeinflusst. Die Option sollte nicht bei jedem Start verwendet werden, weil sie den Vorgang unnötig verlängert.
Nach dem Build kontrollieren Sie, ob das App-Image tatsächlich aus dem gewünschten Verzeichnis stammt:
docker image ls
Für die Diagnose einzelner Dienste ist ein gezielter Start hilfreich:
docker compose up qdrant ollama
So lässt sich unterscheiden, ob der Fehler im Modellserver, in der Vektorsuche oder erst beim Start der Webanwendung entsteht.
Wenn die Anwendung bereits läuft, testen Sie den Containerstatus und die letzten Meldungen:
docker compose ps
docker compose logs --tail=100 winy
Ein Container mit dem Status Exited wurde beendet. Die Ursache steht normalerweise in den letzten Logzeilen. Häufige Auslöser sind ein fehlerhafter Python-Import, ein ungültiger Startbefehl oder ein fehlendes Konfigurationsfeld.
Für wiederholbare Abläufe sollten Sie die Compose-Datei vor dem Start rendern und auf Syntaxfehler prüfen:
docker compose config
Dieser Befehl zeigt die zusammengeführte Konfiguration einschließlich ersetzter Variablen. Dadurch werden Tippfehler in Dateipfaden, leere Werte und falsch eingerückte YAML-Blöcke sichtbar.
- Erster Aufbau: docker compose up --build
- Normaler Hintergrundstart: docker compose up -d
- Status prüfen: docker compose ps
- Logs verfolgen: docker compose logs -f
- Stack beenden: docker compose down
Nach einer erfolgreichen Bereitstellung öffnen Sie die Anwendung im Browser und führen mehrere echte Suchfragen aus. Prüfen Sie dabei Antwortbezug zum Dokumentbestand, relevante Treffer sowie den Umgang mit leeren oder unbekannten Fragen.
Streamlit-Oberfläche unter Port 8501 aufrufen
Öffnen Sie im Browser die Adresse http://localhost:8501. Streamlit stellt die Anwendung dort bereit, sofern der veröffentlichte Host-Port auf den internen Port der Anwendung zeigt. Die Portnummer in der URL gehört zum Docker-Host, nicht zum internen Compose-Netzwerk.
Beim ersten Aufruf kann die Oberfläche kurz verzögert erscheinen. Ursache sind häufig noch laufende Initialisierungen oder das Laden von Modell- und Python-Komponenten. Aktualisieren Sie die Seite nicht mehrfach hintereinander, sondern prüfen Sie zunächst Status und Startprotokolle.
Falls der Browser keine Verbindung aufbaut, grenzen Sie den Fehler systematisch ein:
- Rufen Sie die Adresse mit http:// und nicht mit https:// auf.
- Prüfen Sie, ob auf dem Host bereits ein anderer Dienst Port 8501 verwendet.
- Kontrollieren Sie, ob Streamlit tatsächlich auf 0.0.0.0 lauscht.
- Testen Sie die veröffentlichte Schnittstelle mit curl http://localhost:8501.
- Lesen Sie die letzten Anwendungsprotokolle auf Bindungs- oder Startfehler.
Wird Port 8501 bereits belegt, ändern Sie nur den Host-Port, zum Beispiel:
ports:
- "8502:8501"
Danach verwenden Sie http://localhost:8502. Der rechte Wert bleibt unverändert, weil Streamlit im Container weiterhin auf Port 8501 lauscht.
Für den Zugriff von einem anderen Gerät im lokalen Netz reicht localhost nicht aus. Verwenden Sie stattdessen die IP-Adresse des Docker-Hosts, etwa http://192.168.1.20:8501. Eine solche Freigabe sollte nur in vertrauenswürdigen Netzen erfolgen.
Die Streamlit-Oberfläche eignet sich zunächst für einen Funktionstest. Stellen Sie eine Frage, die nachweisbar im Dokumentbestand beantwortet wird, und danach eine Frage außerhalb des Wissensbereichs. Eine gute Anwendung weist bei fehlendem Kontext darauf hin, statt Sicherheit zu simulieren.
Für einen Zugriff über eine Domain oder eine verschlüsselte Verbindung setzen Sie einen Reverse-Proxy vor Streamlit. Er übernimmt TLS, Zugriffsschutz und gegebenenfalls eine Begrenzung der Anfragegröße. Der direkte Port sollte dann möglichst nur lokal oder innerhalb des internen Netzes erreichbar sein.
Typische Verbindungsprobleme zwischen den Containern beheben
Die meisten Fehler entstehen durch falsche Adressen, nicht bereite Dienste oder fehlende Rechte. Arbeiten Sie die Prüfung deshalb aus Sicht des betroffenen Containers ab. Ein Test auf dem Host beweist nicht, dass die Anwendung denselben Dienst erreichen kann.
1. Dienstnamen und Ports unterscheiden
Innerhalb eines Compose-Netzwerks verwenden Container den Servicenamen und den internen Zielport. Die veröffentlichte Host-Adresse spielt für diese Verbindung keine Rolle. Eine Anwendung im Container greift daher auf http://qdrant:6333 zu, nicht auf http://localhost:6333. Für den Modellserver gilt dasselbe Prinzip.
Zeigt ein Fehler auf eine Host-IP oder auf localhost, prüfen Sie zuerst die effektiven Variablen:
docker compose exec winy env | grep -E 'QDRANT|OLLAMA'
Stimmt der Wert nicht, liegt die Ursache meist in einer falsch benannten Variable, einer nicht geladenen .env-Datei oder einer überschriebenen Compose-Konfiguration.
2. DNS und Netzwerk testen
Ermitteln Sie, ob der App-Container die Namen der Zieldienste auflösen kann:
docker compose exec winy getent hosts qdrant
Falls getent im Image fehlt, starten Sie einen kurzlebigen Prüfcontainer im gleichen Netzwerk. Ein fehlender DNS-Eintrag weist auf unterschiedliche Netzwerke, einen falschen Servicenamen oder ein nicht gestartetes Compose-Projekt hin. Ein Ping ist dagegen wenig aussagekräftig.
3. HTTP-Endpunkte direkt abfragen
Ist der Name auflösbar, testen Sie den Dienst auf Anwendungsebene. Für Qdrant eignet sich der Health-Endpunkt:
curl -fsS http://qdrant:6333/healthz
Für den Modellserver rufen Sie dessen verfügbare API oder den Modellstatus ab. connection refused bedeutet gewöhnlich, dass der Prozess noch nicht lauscht oder auf einer anderen Adresse gebunden ist. Ein Timeout deutet eher auf Routing, Firewall oder eine falsche externe Route hin.
4. Startbereitschaft sauber prüfen
Ein gestarteter Container ist nicht automatisch bereit. Ergänzen Sie deshalb Healthchecks:
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:6333/healthz || exit 1"]
interval: 10s
timeout: 5s
retries: 12
Bei Images ohne curl muss der Test ein vorhandenes Werkzeug nutzen oder ein kleines Prüfskript enthalten. Ein Modellserver braucht beim ersten Laden oft länger als ein Datenbankprozess.
5. Typische Fehlermeldungen richtig deuten
- „Name or service not known“: Servicename oder Netzwerkanbindung ist falsch.
- „Connection refused“: Zielprozess lauscht noch nicht oder am falschen Interface.
- „Connection timed out“: Route, Firewall oder externe Adresse prüfen.
- HTTP 404: Host ist erreichbar, aber der API-Pfad stimmt nicht.
- HTTP 401 oder 403: Zugangsdaten, Proxy oder Zugriffsschutz kontrollieren.
- Leere Suchergebnisse: Dienst erreichbar, Collection jedoch leer oder falsch benannt.
6. Ressourcen und Limits kontrollieren
Wenn Verbindungen während der Anfrage abbrechen, prüfen Sie Speicher und Prozessstatus:
docker stats
docker compose ps
Ein durch den OOM-Killer beendeter Prozess wirkt aus Sicht der Anwendung oft wie ein Netzwerkfehler. Auch eine überlastete CPU kann Zeitüberschreitungen auslösen. Setzen Sie für Clients angemessene Timeouts und behandeln Sie vorübergehende Fehler mit wenigen, begrenzten Wiederholungen. Endloses Retries machen aus einem kleinen Problem schnell eine Lastlawine.
Bei externen Modellservern kommen TLS-Zertifikate, Proxy-Variablen und Firewall-Regeln hinzu. Prüfen Sie die Verbindung mit demselben Container, aus dem die Anfrage stammt. Erst wenn DNS, TCP-Verbindung, HTTP-Status und Modellname einzeln funktionieren, sollte die komplette RAG-Anfrage getestet werden.
Fazit: RAG-Stack starten, testen und dauerhaft speichern
Ein funktionsfähiger Docker-Stack ist erst dann einsatzbereit, wenn drei Dinge zusammenpassen: Die Anwendung beantwortet reale Fragen, die Wissensbasis bleibt nach einem Neustart erhalten und der Betrieb lässt sich nachvollziehbar prüfen. Ein grüner Containerstatus allein reicht dafür nicht.
Führen Sie zum Abschluss einen kleinen Abnahmetest durch. Verwenden Sie eine Frage mit eindeutigem Dokumentbezug, eine bewusst unvollständige Frage und eine Anfrage außerhalb des Datenbestands. Prüfen Sie jeweils Antwortinhalt, Quellenbezug, Antwortzeit und Verhalten bei fehlendem Kontext. So erkennen Sie Halluzinationen, leere Suchergebnisse und zu großzügige Prompt-Einstellungen, bevor andere Nutzer darauf vertrauen.
Dokumentieren Sie außerdem die technische Grundlage des Laufs:
- verwendete Image-Tags und Git-Version
- Modellname und Quantisierung
- Embedding-Modell und Vektordimension
- Chunk-Größe, Überlappung und Anzahl der Treffer
- Speicherorte und letzter Sicherungszeitpunkt
- gemessene Antwortzeit bei typischen Fragen
Diese Angaben machen Ergebnisse reproduzierbar. Ändert sich später ein Modell, ein Parser oder die Chunk-Logik, lässt sich der Einfluss auf Trefferqualität und Kosten besser einordnen.
Für die dauerhafte Speicherung sollten Sie drei Ebenen getrennt betrachten: Container, Volumes und Sicherungen. Container dürfen jederzeit neu erstellt werden. Volumes halten Laufzeitdaten. Sicherungen schützen vor Verlust, Fehlbedienung und beschädigten Daten. Testen Sie eine Wiederherstellung regelmäßig in einer isolierten Umgebung; ein Backup, das nie restauriert wurde, ist eher eine Hoffnung als ein Nachweis.
Behalten Sie auch die Ausgaben unter Last im Blick. Messen Sie Speicherverbrauch, CPU- oder GPU-Auslastung, Fehlerraten und die Zeit bis zum ersten Token. Ein kleiner lokaler Test kann mit 2 bis 4 parallelen Anfragen funktionieren, während dieselbe Konfiguration bei 20 Anfragen ins Stocken gerät. Begrenzen Sie daher gleichzeitige Aufträge und definieren Sie klare Timeouts.
Bei geschäftlicher Nutzung gehören Zugriffsrechte, Löschkonzepte und Protokollierung in die Betriebsdokumentation. Personenbezogene Inhalte sollten nicht länger als nötig gespeichert werden. Für KI-Ausgaben gelten außerdem je nach Einsatzbereich besondere Transparenz- und Risikopflichten. Der EU AI Act ist seit 2024 in Kraft und wird stufenweise angewendet; prüfen Sie den konkreten Anwendungsfall und die jeweils geltenden Fristen.
Eine klare Kennzeichnung von KI-generierten Antworten ist oft sinnvoll, selbst wenn keine spezielle Pflicht greift. Sie schafft Erwartungen, ersetzt aber keine fachliche Kontrolle. Gerade ein RAG-System kann überzeugend formulieren und dennoch eine veraltete oder falsch verstandene Textstelle verwenden.
Für die technische Dokumentation sollten Sie neben der Compose-Datei auch Start-, Prüf- und Wiederherstellungsbefehle festhalten. Neue Teammitglieder können den Stack damit ohne mündliche Übergabe reproduzieren. Damit ist die Implementierung nicht nur gestartet, sondern belastbar eingeordnet: Antworten wurden geprüft, Betriebsdaten festgehalten und die dauerhafte Speicherung verifiziert.
Häufige Fragen zur Docker-basierten RAG-Implementierung
Was ist ein RAG-System in Docker?
Ein RAG-System in Docker kombiniert eine Webanwendung, eine Vektordatenbank und einen Modellserver in getrennten Containern. Die Anwendung sucht zunächst passende Dokumentabschnitte und übergibt diese anschließend zusammen mit der Nutzerfrage an ein Sprachmodell.
Welche Komponenten benötigt eine Docker-basierte RAG-Anwendung?
Typischerweise benötigt die Anwendung eine Benutzeroberfläche wie Streamlit, eine Vektordatenbank für Embeddings, beispielsweise Qdrant, sowie einen Modellserver wie Ollama. Docker Compose verbindet diese Dienste über ein gemeinsames Netzwerk.
Warum darf die RAG-Anwendung innerhalb von Docker nicht localhost verwenden?
Innerhalb eines Containers verweist localhost auf den Container selbst. Für die Kommunikation mit anderen Diensten müssen die Compose-Servicenamen und die internen Ports verwendet werden, zum Beispiel http://qdrant:6333 für die Vektordatenbank oder http://ollama:11434 für den Modellserver.
Wie bleiben Vektordatenbank und Modelle nach einem Neustart erhalten?
Dafür müssen persistente Docker-Volumes eingerichtet werden. Qdrant speichert seine Daten beispielsweise unter /qdrant/storage, während Ollama seine Modelle unter /root/.ollama ablegt. Ein Volume schützt die Daten vor dem Löschen oder Neuerstellen eines Containers, ersetzt jedoch kein Backup.
Wie wird ein Docker-basierter RAG-Stack gestartet und getestet?
Im Projektverzeichnis wird der Stack mit docker compose up --build gestartet. Anschließend ist die Weboberfläche normalerweise unter http://localhost:8501 erreichbar. Für die Prüfung sollten Containerstatus und Logs kontrolliert, Modell- und Datenbankverbindungen getestet sowie Fragen mit vorhandenem und fehlendem Dokumentbezug gestellt werden.





