Lokaler KI-Server mit 2× Tesla T4: von OpenClaw zu Open WebUI
Zwei Tesla T4, eine Debian-VM und ein Plan. Wie aus einem autonomen Agenten, der Unsinn erzählt, ein lokales ChatGPT wurde, das funktioniert.
Ich hatte zwei NVIDIA Tesla T4 übrig, eine frische VM und einen Plan: ein eigener KI-Server im Homelab. Lokale Sprachmodelle, ein autonomer Agent mit Discord-Anbindung, eine Weboberfläche für mehrere Leute. Alles ohne Cloud.
Dieser Beitrag erzählt, wie es wirklich gelaufen ist. Mit allen Irrwegen, denn die waren lehrreicher als das Ergebnis.
Die Ausgangslage
| Komponente | Details |
|---|---|
| Plattform | VM „AI-01“, Debian 13 „trixie“, Kernel 6.12 |
| CPU | 32 vCPUs (Host: Intel Xeon E5-2695, 2,1 GHz) |
| RAM | ca. 100 GB (der Host hätte noch rund 400 GB mehr) |
| Festplatte | ca. 985 GB |
| GPUs | 2× NVIDIA Tesla T4 mit je 16 GB VRAM, per PCI-Passthrough |
| Secure Boot | aus, die NVIDIA-Module müssen also nicht signiert werden |
GPUs zum Leben erwecken
lspci zeigte beide T4, aber nvidia-smi gab es nicht. Also erstmal die Treiber von Nvidia selbst herunterladen.
In diesem fall müssen wir noch das non-free im Debian Repo akivieren:
/etc/apt/sources.list.d/debian.sources (deb822-Format):
Components: main contrib non-free non-free-firmware
Danach den Treiber installieren:
sudo apt update
sudo apt install linux-headers-amd64 nvidia-driver nvidia-smi firmware-misc-nonfree
Debian liefert Treiber 550.163.01 (CUDA bis 12.4). Das Kernelmodul wird per DKMS gebaut, nouveau automatisch gesperrt und das initramfs neu erzeugt. Danach ist ein Neustart fällig.
Nach dem Reboot meldete nvidia-smi beide Karten: 49 °C, 10–12 W im Leerlauf, je 15 GB frei.
Ollama: der einfache Teil
curl -fsSL https://ollama.com/install.sh | sh
Das war's. Ollama läuft als systemd-Dienst unter eigenem Benutzer, lauscht nur auf 127.0.0.1:11434 und erkennt beide T4 per CUDA. Ein separates CUDA-Toolkit braucht es nicht, Ollama bringt seine Bibliotheken selbst mit.
OpenClaw: ein Agent mit Persönlichkeit
OpenClaw ist ein Open-Source-Agent, der über Discord, Telegram oder WhatsApp erreichbar ist. Er kann Shell-Befehle ausführen, Dateien bearbeiten, im Web suchen und Automationen planen. Dazu gehören ein Gateway-Dienst und eine Weboberfläche.
Erst mal Sicherheit
Mein Benutzer hat passwortloses sudo. Ein Agent, der autonom Shell-Befehle ausführt, hätte damit indirekt Root-Rechte gehabt. Keine gute Idee. Also bekam OpenClaw einen eigenen Benutzer openclaw ohne sudo und ohne Zusatzgruppen. Damit sein systemd-User-Dienst auch ohne Login läuft, wird Lingering aktiviert:
sudo useradd -m -s /bin/bash openclaw
sudo loginctl enable-linger openclaw
Installation und Stolpersteine
Debian trixie liefert ein zu altes Node.js, also kam Node 24 von NodeSource. Das Installskript von OpenClaw habe ich vorher heruntergeladen, dann als openclaw ausgeführt. Weil /usr für diesen Benutzer nicht beschreibbar ist, landete alles in ~/.npm-global.
Beim Onboarding gab es ein paar Fallen (meine eigene Inkompetenz):
- Erst mal eine Shell öffnen und dann mich als
openclawanmelden:sudo -iu openclaw. - Für einen systemd-User-Dienst reicht auch das oft nicht, weil keine echte Login-Session mit D-Bus existiert. Abhilfe schafft
sudo machinectl shell openclaw@(Paketsystemd-container). - Ich habe
sudo machinectl …natürlich erst mal alsopenclawprobiert. Der hat bewusst kein sudo. Alsoexitund noch mal mit meinem User:mace. - „Gateway not detected yet“: Der Gateway braucht rund 30 Sekunden zum Starten, das Onboarding gibt vorher auf. Er lief trotzdem.
Als Modelle kamen qwen3:30b (18 GB) und gemma4:e4b (6,6 GB) auf die Platte. Was ich da noch nicht wusste: qwen3:30b ist bei Ollama die Thinking-Variante. Das wird später wichtig.
Jarvis bekommt einen Charakter
OpenClaw liest bei jedem Sitzungsstart Dateien aus seinem Workspace: AGENTS.md, SOUL.md, IDENTITY.md und USER.md. Mein Agent heißt Jarvis, ist ein lockerer Typ, spricht Deutsch und duzt. In SOUL.md steht: direkt, ehrlich, trockener Humor, „erst die Lösung, dann der Spruch“. Die Sicherheitsgrenzen bleiben: private Daten schützen und vor riskanten Aktionen fragen.
Die Weboberfläche und der Basic-Auth-Albtraum
Die Weboberfläche sollte im LAN erreichbar sein, später auch über NetBird, und natürlich mit Passwort.
Der Aufbau: Der OpenClaw-Gateway bleibt auf 127.0.0.1:18789 und ist nie direkt im Netz. Davor sitzt Caddy als Reverse Proxy mit selbst signiertem Zertifikat (tls internal). HTTP leitet automatisch auf HTTPS um. In OpenClaw kommen die neuen Adressen in gateway.controlUi.allowedOrigins, dazu gateway.trustedProxies: ["127.0.0.1"].
Dann sollte noch Basic Auth davor. Klingt trivial. Die Passwortabfrage kam aber immer wieder, egal was ich eingab. Die Fehlersuche lief über das Access-Log von Caddy:
- WebSockets und
/healthzkamen ohne Zugangsdaten an. Chromium schickt bei WebSocket-Verbindungen kein Basic Auth mit. Also eine Ausnahme dafür. - Anfragen mit
Authorization-Header wurden trotzdem abgelehnt. Die OpenClaw-Oberfläche schickt bei API-AufrufenAuthorization: Bearer <Token>. Das ist derselbe Header wie bei Basic Auth, und ein Browser kann nur einen davon senden. Also noch eine Ausnahme für Bearer-Anfragen. Der Gateway prüft das Token ja selbst. - Immer noch Abfragen. An der Stelle habe ich entschieden: Der Passwortschutz fliegt raus. Caddy macht nur noch HTTPS, geschützt wird über das Gateway-Token und die Gerätefreigabe in OpenClaw.
„Warum erzählt der so viel Müll?“
Jetzt kam der eigentliche Test. Ich fragte Jarvis: „Was macht die Thomas-Krenn.AG?“
Richtig wäre: ein Serverhersteller aus Freyung im Bayerischen Wald, bekannt für individuell konfigurierte Server, einen Online-Konfigurator und das Thomas-Krenn-Wiki.
Jarvis wusste es anders. Erst sei das wohl ein Tippfehler und Thomas Krenn ein IT-Sicherheitsexperte und Linux-Blogger. Auf „du liegst falsch“ war es plötzlich ein Serverhersteller, die „Thomas Krenn GmbH“. Auf „ist eine AG“ stimmte er sofort zu und erklärte mir den Unterschied zwischen AG und GmbH. Später kamen Rechenzentren in München, Frankfurt und Wien dazu, eine Gründung 1985 in Ludwigshafen und Pentests für Banken.
Klassisches Halluzinieren, garniert mit unkritischem Zustimmen. Also auf Fehlersuche.
Ursache 1: Das Kontextfenster war zu klein
Im Gateway-Log stand:
route=compact_only estimatedPromptTokens=26176 promptBudgetBeforeReserve=24576
OpenClaw hatte das Modell auf 32 768 Tokens begrenzt, obwohl Qwen3 über 260 000 kann. Allein Systemprompt und Tools brauchten rund 26 000. Jede Anfrage wurde also zusammengestrichen, bevor das Modell überhaupt meine Frage sah.
Der Fix: contextTokens für das Modell auf 65.536 hochsetzen und Ollama per systemd-Override etwas tunen:
# /etc/systemd/system/ollama.service.d/override.conf
[Service]
Environment="OLLAMA_FLASH_ATTENTION=1"
Environment="OLLAMA_KEEP_ALIVE=30m"
Ergebnis: 26 GB VRAM über beide GPUs verteilt, 100 % GPU, rund 75 Tokens pro Sekunde.
Ursache 2: Ein riesiger Systemprompt
Mit /context detail zeigt OpenClaw, woraus der Prompt besteht:
| Anteil | Tokens |
|---|---|
| Tool-Schemas (54 Tools!) | ~19 300 |
| OpenClaw-Systemprompt | ~4 400 |
| Workspace-Dateien | ~3 250 |
| Skills-Liste (20 Skills, nur Einträge) | ~1 500 |
| Summe | ~28 300 |
Dabei fiel etwas Absurdes auf: IDENTITY.md war von 215 auf 4 415 Zeichen gewachsen. Ich hatte über die Weboberfläche ein Avatarbild gesetzt, und die UI hatte das Bild als Base64 direkt in die Datei geschrieben. Das waren rund 1 100 Tokens Bilddaten in jedem einzelnen Prompt. Das Bild liegt jetzt als Datei daneben, in IDENTITY.md steht nur noch der Pfad.
Danach habe ich per tools.deny alles entfernt, was auf einem Server ohne Bildschirm nichts bringt: Desktop-, Browser-, Canvas- und Sprach-Tools, dazu die Sub-Agenten (Ollama verarbeitet ohnehin nur eine Anfrage gleichzeitig). Ergebnis: ~18 700 statt ~28 300 Tokens, gut ein Drittel weniger.
Mehr RAM bringt keinen größeren Kontext
Mein erster Gedanke: Der Host hat noch 400 GB RAM frei, die gebe ich der VM einfach. Leider hilft das kaum. Passt der Kontext nicht mehr in den VRAM, lagert Ollama Teile auf die CPU aus. Dann bricht die Geschwindigkeit ein, von 75 auf geschätzt 10–25 Tokens pro Sekunde, und das Einlesen langer Prompts wird noch viel zäher.
Der bessere Weg ist, den KV-Cache zu quantisieren. Mit OLLAMA_KV_CACHE_TYPE=q8_0 (setzt Flash Attention voraus) braucht der Cache nur noch rund die Hälfte:
| Kontext | KV-Cache f16 | KV-Cache q8 | Passt auf die GPUs? |
|---|---|---|---|
| 64k | ~6 GB | ~3 GB | ja |
| 128k | ~12 GB | ~6,4 GB | ja (gewählt) |
| 256k | ~24 GB | ~12 GB | nein (wird Sportlich) |
Mit 128k Kontext läuft alles weiter komplett auf den GPUs: 26 GB VRAM, rund 63 Tokens pro Sekunde.
Ursache 3: Das Thinking-Modell „verdenkt“ den Tool-Aufruf
Die Antworten waren trotzdem Unsinn. Also ging es in die SQLite-Datenbank von OpenClaw, wo die Verläufe der Sitzungen liegen. Im Denkprozess des Modells stand sinngemäß: „Ich sollte jetzt web_search mit ‚Thomas-Krenn.AG‘ aufrufen.“ Danach beendete es den Zug, ohne den Aufruf tatsächlich zu senden (non_deliverable_terminal_turn). OpenClaw erzwang dann eine Antwort ohne Tools, und das Modell fing an zu fantasieren.
Die Lösung war der Wechsel auf qwen3:30b-a3b-instruct-2507-q4_K_M: gleiche Größe, kein Thinking. Interessant: In einem direkten Test gegen die Ollama-API mit einem einzigen web_search-Tool lieferten beide Varianten 3 von 3 korrekte Aufrufe. Das Problem trat nur im großen OpenClaw-Kontext auf.
Ursache 4: Falsche Tools, falscher Kanal
Das Instruct-Modell antwortete erst mal ganz ohne Suche. Also kam eine Regel in SOUL.md: Fakten: erst suchen, dann reden. Bei Firmen, Personen, Produkten, Preisen und allem Aktuellen immer zuerst web_search, Quelle nennen, bei Korrekturen erst nachprüfen.
Danach wurde es kurios:
- Jarvis rief
tool_searchmit „Thomas-Krenn AG company overview“ auf. Ergebnis: eine leere Liste.tool_searchdurchsucht aber nicht das Web, sondern OpenClaws eigenen Werkzeugkatalog. Das echteweb_searchwar hinter der Tool-Suche versteckt. - Mit abgeschalteter Tool-Suche griff er zum Tool
intent, das Daueraufträge anlegt. Das scheiterte mit „authenticated sender identity is unavailable“. - Fragte ich ihn nach seinen Tools, nannte er nur Nachrichten- und Sitzungs-Tools.
web_search,readundexecfehlten komplett. - Am Ende stellte er Rückfragen, brauchte über drei Minuten und schickte die Antwort auf eine Frage aus der Weboberfläche nach Discord.
Und dann gab es noch einen peinlichen Zwischenfall. Ich hatte Jarvis zum Testen über die Kommandozeile (openclaw agent) angesprochen. Dabei gab es keinen festen Antwortkanal, also nutzte Jarvis sein message-Tool und postete die Antworten schon wieder in einen zufälligen Discord-Channel. Danach war mit CLI-Tests Schluss.
Die Einsicht: falsches Werkzeug für lokale Modelle
OpenClaw ist für starke Cloud-Modelle wie Claude oder GPT gebaut: ein riesiger Systemprompt, Dutzende Tools, Kanäle, Automationen, Tool-Suche, Regeln für Absender und Besitzer. Ein lokales 30B-Modell ist mit dieser Komplexität überfordert. Und die Tool-Logik war selbst für mich kaum zu durchschauen. Meine Vermutung, nicht bestätigt: Die Sitzung aus der Weboberfläche wurde nicht als Besitzer erkannt, weil nur meine Discord-ID hinterlegt war.
Am Ende standen drei Optionen auf dem Tisch:
- A: Open WebUI als lokales „ChatGPT“ mit Websuche und echten Benutzerkonten
- B: OpenClaw mit einem Cloud-Modell. Das kostet Geld, und die Daten verlassen das Haus.
- C: Weiter debuggen, mit ungewissem Ausgang
Ich habe mich für A entschieden und OpenClaw komplett vom Server entfernt: Dienst gestoppt, Lingering aus, Benutzer samt Home gelöscht, Logs weg, Node.js, das NodeSource-Repo und systemd-container deinstalliert. Eine abschließende Suche fand keine Reste.
Open WebUI: „Juhuuuuu“
Open WebUI läuft als Docker-Container. Docker kommt direkt aus Debian (docker.io):
sudo apt install docker.io
sudo docker run -d --name open-webui \
--restart unless-stopped \
--network host \
-v open-webui:/app/backend/data \
-e PORT=8080 \
-e HOST=127.0.0.1 \
-e OLLAMA_BASE_URL=http://127.0.0.1:11434 \
-e WEBUI_SECRET_KEY=<ein-langer-zufälliger-wert> \
-e WEBUI_NAME=AI-01 \
-e DEFAULT_USER_ROLE=pending \
-e ENABLE_WEB_SEARCH=true \
-e WEB_SEARCH_ENGINE=duckduckgo \
-e WEB_SEARCH_RESULT_COUNT=5 \
-e ANONYMIZED_TELEMETRY=false \
ghcr.io/open-webui/open-webui:main
Die wichtigsten Punkte:
- Der Container lauscht nur auf
127.0.0.1:8080und ist von außen nicht erreichbar. Caddy zeigt jetzt dorthin, mit HTTPS und einem Upload-Limit von 200 MB. - Neue Benutzer können sich registrieren, landen aber im Status „pending“, bis ich sie freischalte.
- Die Websuche läuft über DuckDuckGo, ganz ohne API-Key.
Dann kam dieselbe Frage, mit demselben Modell auf derselben Hardware: „Was macht die Thomas-Krenn.AG?“
Das Modell rief selbstständig die Websuche auf und antwortete im Kern richtig: Sitz in Freyung, individuell konfigurierte Server, Tests vor der Auslieferung, „Servers Made in Germany“. Meine Reaktion im Chat war ein schlichtes „Juhuuuuu“.
Das ist für mich die wichtigste Erkenntnis aus dem ganzen Projekt: Das Modell war nie das Problem. Mit einem schlanken Frontend, das sich auf Chat und Websuche konzentriert, funktioniert es. Mit einem überladenen Agenten-Framework nicht.
Nebenschauplätze
Programmieren mit dem eigenen Modell
Cursor erlaubt zwar eine eigene OpenAI-kompatible Adresse, schickt die Anfragen aber über die eigenen Server. Eine Adresse im LAN ist von dort nicht erreichbar, man müsste den KI-Server öffentlich ins Internet stellen. Das will ich nicht.
Besser passt VS Code mit der Erweiterung Continue (oder Cline). Die spricht direkt vom PC aus die OpenAI-kompatible API von Open WebUI unter https://<server>/api an, mit einem persönlichen API-Key. Die Keys muss der Admin in Open WebUI erst freigeben. Wegen des selbst signierten Zertifikats importiert man die Root-CA von Caddy auf dem PC oder schaltet die Zertifikatsprüfung in Continue ab.
Für Code ist qwen3-coder:30b die bessere Wahl. Es ist genauso groß und passt ebenfalls auf die beiden T4.
Der Endstand
| Dienst | Details |
|---|---|
| NVIDIA | Treiber 550.163.01, CUDA 12.4, DKMS |
| Ollama | systemd, nur lokal, Flash Attention, KV-Cache q8_0, Keep-Alive 30 min |
| Hauptmodell | qwen3:30b-a3b-instruct-2507-q4_K_M mit 128k Kontext |
| Open WebUI | Docker, nur lokal, Konten mit Freischaltung, Websuche über DuckDuckGo |
| Caddy | HTTPS mit interner CA, Reverse Proxy auf Open WebUI |
| OpenClaw | vollständig entfernt |
Auf der Liste steht noch: NetBird auf der VM einrichten, die Registrierung in Open WebUI eventuell ganz abschalten, qwen3-coder mit Continue ausprobieren und die ungenutzten Modelle löschen. Das sind rund 25 GB.
Was ich gelernt habe
- Agenten nie mit sudo: Ein autonomer Agent bekommt einen eigenen Benutzer ohne Rechte.
- Basic Auth und moderne Web-Apps vertragen sich nicht.
- Kontextgröße prüfen: Frameworks setzen oft vorsichtige Standardwerte. Hier waren es 32k, und das Modell sah nur einen Bruchteil seiner Anweisungen.
- Mehr RAM heißt nicht mehr Kontext. Ein quantisierter KV-Cache (q8_0) verdoppelt den Kontext im selben VRAM, fast ohne Verlust.
- Base64-Bilder in Prompt-Dateien fressen still und leise Tokens.
- Thinking-Modelle können Tool-Aufrufe „verdenken“. Für Tools lieber die Instruct-Variante nehmen.
- Lokale Modelle und überladene Frameworks ergeben Halluzinationen. Weniger Tools und eine klare Oberfläche bringen bessere Ergebnisse.
- Logs lesen statt raten: Die echten Ursachen standen im Gateway-Log und in der Sitzungsdatenbank, nicht in der Doku.
- Manchmal ist das Werkzeug falsch, nicht die Konfiguration. Open WebUI lief auf Anhieb.
Unterm Strich habe ich jetzt ein eigenes, lokales ChatGPT für mich und ein paar Leute, das weiß, wann es nachschauen muss. Und Jarvis? Der ist erst mal in Rente. Vielleicht kommt er wieder, wenn lokale Modelle groß genug für ihn sind.