Tech
Lokale KI in Docker mit Ollama und ComfyUI
Nachdem ich jetzt ja eine rechte gute Grafikkarte mit 16GB Ram hab - eine Nvidia RTX 5070 Ti, über die ich ja bereits berichtet habe - kann ich endlich mal ausprobieren, wie man lokal mit KIs arbeitet. Die richtig großen Modelle laufen natürlich in irgendwelchen Rechenzentren. Aber auch mit den kleinen kostenlosen Modellen kann man bereits sehr viel umsetzen und ausprobieren. Es hat sich in letzter Zeit auch einiges getan und mit den kleineren, quantisierten Modellen kommt man auf einer halbwegs aktuellen Consumer-Grafikkarte schon erstaunlich weit.
Der große Vorteil dabei: Alles läuft bei mir zu Hause, meine Daten verlassen den Rechner nicht und ich muss auch kein Abo abschließen. Und ganz ehrlich - es macht einfach Spaß, dem Ganzen mal unter die Haube zu schauen.
Wie man mich kennt, will ich natürlich nichts direkt auf dem System installieren, sondern alles schön sauber in Docker kapseln. Das hat sich bei mir einfach bewährt: schnell gesichert, schnell umgezogen und wenn mal was schiefläuft, ist der Container schnell weggeworfen und neu gebaut, ohne dass ich mir mein Hostsystem zumülle.
Vorbereitung: GPU im Container
Bevor es losgeht, gibt es allerdings eine Kleinigkeit zu beachten. Damit ein Docker-Container überhaupt auf die Grafikkarte zugreifen kann, braucht man das NVIDIA Container Toolkit. Ohne das läuft zwar auch alles, aber eben nur auf der CPU und das ist bei KI-Modellen ungefähr so schnell wie wenn man auf dem Pilgerweg nach Santiago de Compostela zu Fuss unterwegs ist.
Auf einem Ubuntu-System installiert man das folgendermaßen:
$ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
$ curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
$ sudo apt update
$ sudo apt install -y nvidia-container-toolkit
$ sudo nvidia-ctk runtime configure --runtime=docker
$ sudo systemctl restart docker
Ob das Ganze funktioniert, kann man mit einem einfachen Testcontainer prüfen:
$ docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu24.04 nvidia-smi
Wenn hier die gewohnte Ausgabe von nvidia-smi mit der eigenen Grafikkarte erscheint, dann sieht der Container die GPU und wir können loslegen.
In meinem Compose-Stack sieht man das an dem deploy-Block, der bei den GPU-Services steht. Damit reserviere ich für den jeweiligen Container die Grafikkarte:
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
Der Docker Compose Stack
Statt jeden Container einzeln von Hand zu starten, hab ich das Ganze - wie man es mittlerweile so macht - in eine compose.yaml gepackt. So habe ich alles an einem Ort und kann den kompletten Stack mit einem einzigen docker compose up -d hochfahren. Hier also erst einmal die gesamte Datei, die einzelnen Dienste erkläre ich dann weiter unten:
services:
ollama:
image: ollama/ollama:latest
container_name: ollama
restart: unless-stopped
environment:
- OLLAMA_DEBUG=1
# WICHTIG: Erlaubt SillyTavern den Zugriff
- OLLAMA_ORIGIN=*
ports:
- "11434:11434"
volumes:
- ./ollama:/root/.ollama
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
comfyui:
image: mmartial/comfyui-nvidia-docker:ubuntu24_cuda13.2-latest
container_name: comfyui
restart: unless-stopped
ports:
- "8188:8188"
environment:
- WANTED_UID=1000
- WANTED_GID=1000
- TZ=Europe/Berlin
- COMFY_HOME=/basedir
- COMFY_CMDLINE_EXTRA=--listen 0.0.0.0
- SECURITY_LEVEL=weak
- USE_NEW_MANAGER=true
- TORCH_WEIGHTS_ONLY_LOAD=0
volumes:
- ./comfyui:/comfy/mnt
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
anythingllm:
image: mintplexlabs/anythingllm:master
container_name: anythingllm
restart: unless-stopped
user: "1000:1000"
ports:
- "3001:3001"
cap_add:
- SYS_ADMIN
environment:
- STORAGE_DIR=/app/server/storage
- OLLAMA_BASE_URL=http://ollama:11434
volumes:
- ./anythingllm:/app/server/storage
- ./my_epubs:/app/server/storage/documents/import:ro
depends_on:
- ollama
open-webui:
image: ghcr.io/open-webui/open-webui:main
container_name: open-webui
volumes:
- ./open-webui:/app/backend/data
depends_on:
- ollama
- comfyui
- anythingllm
ports:
- "3000:8080"
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- ENABLE_IMAGE_GENERATION=True
- IMAGE_GENERATION_ENGINE=comfyui
- COMFYUI_BASE_URL=http://comfyui:8188
restart: unless-stopped
sillytavern:
image: ghcr.io/sillytavern/sillytavern:latest
container_name: sillytavern
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- ./sillytavern/config:/home/node/app/config
- ./sillytavern/data:/home/node/app/data
depends_on:
- ollama
Man sieht relativ schnell, dass ich hier ein paar verschiedene Bausteine zusammengesteckt habe. Wichtig ist mir bei allen Diensten, dass ich die Daten über Volumes nach außen lege (die ./ollama, ./comfyui usw.). So bleiben mir die heruntergeladenen Modelle, meine Chats und Einstellungen erhalten, auch wenn ich mal einen Container neu baue.
Jetzt aber der Reihe nach zu den einzelnen Diensten.
Ollama - der Motor im Hintergrund
Ollama ist sozusagen das Herzstück des ganzen Stacks. Es ist die Software, die die eigentlichen Sprachmodelle (die LLMs) lädt und ausführt. Man kann sich das ein bisschen wie eine Datenbank vorstellen, nur dass eben statt Tabellen die KI-Modelle verwaltet werden. Ollama kümmert sich darum, dass die Modelle auf die Grafikkarte geladen werden, und stellt eine schöne, einfache API bereit, über die dann alle anderen Dienste ihre Anfragen schicken.
Ein Modell installiert man dabei denkbar einfach. Man geht in den Container und lädt sich sein Wunschmodell:
$ docker exec -it ollama ollama pull llama3.1:8b
Das :8b steht dabei für die Anzahl der Parameter (hier 8 Milliarden). Als Faustregel gilt: Je größer die Zahl, desto klüger das Modell, aber desto mehr Grafikspeicher braucht es auch. Mit meinen 16GB komme ich bei den 7er- und 8er-Modellen sehr gut zurecht, und selbst ein 14b-Modell läuft in quantisierter Form noch flüssig. Bei den ganz großen Brocken geht dann irgendwann der Speicher aus und Ollama lagert auf die CPU aus - und dann wird es eben zäh.
Die Umgebungsvariable OLLAMA_ORIGIN=* ist übrigens - wie im Kommentar vermerkt - dafür da, dass sich Clients wie SillyTavern von außen verbinden dürfen. Ohne die blockt Ollama die Anfragen aus fremden Origins gerne mal ab.
Ollama selbst hat keine schicke Oberfläche, es ist wirklich nur der Motor. Um damit zu arbeiten, brauchen wir noch eine Bedienoberfläche - und da kommen die nächsten Dienste ins Spiel.
Open WebUI - das ChatGPT für zu Hause
Open WebUI ist meine zentrale Anlaufstelle und sieht optisch dem bekannten ChatGPT sehr ähnlich. Man hat links seine Chatverläufe, oben wählt man das Modell aus und kann dann einfach drauflosschreiben. Für den alltäglichen Gebrauch - mal eben eine E-Mail formulieren lassen, ein Stück Code erklären oder einfach nur ein bisschen herumspielen - ist das genau das Richtige.
Das Schöne ist, dass Open WebUI der Klebstoff zwischen den anderen Diensten ist. In meiner Konfiguration sieht man das gut an den ganzen Umgebungsvariablen:
- über
OLLAMA_BASE_URLredet es mit Ollama und bekommt damit Zugriff auf alle meine installierten Sprachmodelle, - mit
ENABLE_IMAGE_GENERATION=TrueundIMAGE_GENERATION_ENGINE=comfyuikann ich mir direkt aus dem Chat heraus Bilder generieren lassen - im Hintergrund arbeitet dann ComfyUI,
Damit habe ich praktisch eine eigene kleine KI-Zentrale, die Text und Bild kann, und das alles komplett offline auf meinem eigenen Rechner. Erreichbar ist das Ganze dann unter Port 3000.
ComfyUI - die Bildmaschine
Während Ollama sich um Text kümmert, ist ComfyUI für die Bildgenerierung zuständig. Hier laufen Modelle wie Stable Diffusion oder Flux, mit denen man aus einer Textbeschreibung Bilder erzeugen kann.
ComfyUI hat einen ganz eigenen Ansatz, der mich als Entwickler sofort angesprochen hat: Man baut sich den Ablauf als eine Art Flussdiagramm zusammen. Man zieht sich einzelne Knoten (Nodes) auf die Arbeitsfläche - einen für das Modell, einen für den Prompt, einen für den Sampler und so weiter - und verbindet die mit Linien. Das wirkt am Anfang erschlagend, aber man hat dadurch eine wahnsinnige Kontrolle über jeden einzelnen Schritt. Wer einfach nur schnell ein Bild will, ist mit der Anbindung über Open WebUI (siehe oben) besser bedient; wer aber wirklich in die Tiefe gehen und an allen Rädchen drehen will, kommt an ComfyUI unter Port 8188 nicht vorbei.
Ein paar Worte zu den Umgebungsvariablen: Mit WANTED_UID/WANTED_GID sorge ich dafür, dass die Dateien im gemounteten Volume mit meinem eigenen Benutzer und nicht als root angelegt werden - das erspart einem hinterher viel Ärger mit den Rechten. SECURITY_LEVEL=weak und TORCH_WEIGHTS_ONLY_LOAD=0 sind nötig, damit man auch Modelle und benutzerdefinierte Nodes aus der Community laden kann. Das würde ich auf einem öffentlich erreichbaren Server so natürlich nicht machen, aber in meinem abgeschotteten Heimnetz ist das völlig in Ordnung.
AnythingLLM - meine private Wissensdatenbank
AnythingLLM geht in eine ganz andere Richtung und ist für mich fast das spannendste Stück im ganzen Stack. Hier geht es um das Stichwort RAG (Retrieval-Augmented Generation). Vereinfacht gesagt: Man wirft eine Menge eigener Dokumente hinein, und die KI kann dann Fragen dazu beantworten, indem sie in genau diesen Dokumenten nachschlägt.
Man sieht in meiner Konfiguration, dass ich einen Ordner ./my_epubs schreibgeschützt hineinmounte:
volumes:
- ./anythingllm:/app/server/storage
- ./my_epubs:/app/server/storage/documents/import:ro
Da liegen bei mir ein paar E-Books drin, und ich kann der KI dann Fragen zum Inhalt stellen, ohne dass ich die Bücher selbst durchblättern muss. Genau wofür man RAG eben verwendet: eine private Wissensbasis, sei es nun die eigene Dokumentensammlung, technische Handbücher oder was auch immer man so an Text herumliegen hat. Im Hintergrund werden die Dokumente in kleine Häppchen zerlegt und in einer Vektordatenbank abgelegt (den auskommentierten Chroma-Teil hab ich mir für später aufgehoben, das eingebaute LanceDB reicht mir erst mal). Für die eigentliche Antwort greift AnythingLLM dann wieder auf Ollama zurück. Die Oberfläche findet man unter Port 3001.
SillyTavern - zum Spielen und für Rollenspiele
Zu guter Letzt noch SillyTavern. Das ist ein ganz anderes Kaliber und dient in erster Linie der Unterhaltung. Ursprünglich kommt das Ganze aus der Ecke der KI-Charaktere und Rollenspiele: Man legt sich Figuren mit einer eigenen Persönlichkeit, Hintergrundgeschichte und Sprechweise an und kann sich dann mit denen unterhalten.
Auch wenn das auf den ersten Blick verspielt wirkt, ist es zum Ausprobieren gar nicht so verkehrt. Man bekommt nämlich ein sehr gutes Gefühl dafür, wie stark ein sogenanntes System-Prompt das Verhalten eines Modells beeinflusst und wie unterschiedlich sich die einzelnen Modelle bei der gleichen Figur anstellen. Vor allem im Hinblick darauf, dass ich ein Tool für Pen&Paper Abenteuer erstellen will, ist das eine gute Quelle um zu erkennen, wie die Dinge funktionieren.
SillyTavern hängt sich - wie die anderen auch - hinten an Ollama und läuft bei mir unter Port 8000. Das ist übrigens genau der Dienst, für den die weiter oben erwähnte OLLAMA_ORIGIN=*-Einstellung nötig war.
Fazit
Ich muss sagen, ich bin ziemlich begeistert, wie weit man mittlerweile mit lokaler KI auf ganz normaler Consumer-Hardware kommt. Vor ein paar Jahren war das noch undenkbar, und jetzt läuft bei mir eine ganze kleine KI-Werkstatt, ohne dass ich auch nur ein einziges Byte an irgendeinen Cloud-Anbieter schicke.
Die Grafikkarte mit den 16GB ist dabei natürlich der begrenzende Faktor - die ganz großen Modelle bleiben mir verwehrt - aber für alles, was ich so vorhabe, reicht es locker aus und die Geschwindigkeit ist überraschend gut. Und das Beste am ganzen Docker-Ansatz: Wenn morgen ein neuer, spannender Dienst herauskommt, dann hänge ich einfach einen weiteren Service in die compose.yaml und probiere ihn aus. Falls er mir nicht gefällt, ist er genauso schnell wieder verschwunden.
Ich denke, in den nächsten Wochen werde ich noch das eine oder andere ausprobieren und wenn sich etwas Interessantes ergibt, dann schreibe ich sicher wieder darüber.