| caddy | ||
| client | ||
| docs | ||
| llama-swap | ||
| scripts | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
LocalAI – zentraler LLM-Server für opencode, Continue & Laravel
Ein Docker-Compose-Stack, der auf alegab-pc (RX 6950 XT 16 GB, Ryzen 9 7900X3D, 62 GB RAM) eine OpenAI-kompatible API bereitstellt. Clients im Heim-LAN und über NetBird nutzen sie zum Programmieren, ganz ohne Cloud-KI.
Client-PC (opencode / VS Code + Continue) ──LAN / NetBird──┐
▼
caddy (Host-Netz, :8800) IP-Filter + API-Key
└─► llama-swap (nur 127.0.0.1:8080) lädt/wechselt Modelle
├─► llama-server (llama.cpp, ROCm) Chat, Autocomplete, Embeddings
├─► whisper-server (whisper.cpp, Vulkan) Transkription
└─► sd-server (stable-diffusion.cpp, Vulkan) Bilder
Modelle: ./models (LLMs per -hf beim ersten Aufruf, whisper/ und sd/ manuell)
| Modell-ID | Zweck | Verhalten |
|---|---|---|
qwen3.5-35b-a3b |
Hauptmodell Chat/Agent (MoE) | eines der Chat-Modelle gleichzeitig geladen |
glm-4.7-flash |
Alternative Chat/Agent (MoE) | 〃 |
qwen3.5-9b |
schnelles kleines Chat-Modell | 〃 |
qwen2.5-coder-1.5b |
Autocomplete (FIM) für Continue | dauerhaft geladen (GPU) |
nomic-embed |
Embeddings für Continue @codebase |
dauerhaft geladen (CPU) |
whisper |
Transkription (/v1/audio/transcriptions), Alias whisper-1 |
dauerhaft geladen (GPU, ~1,2 GB) |
z-image |
Bilder (/v1/images/generations), Aliase gpt-image-1, dall-e-3, dall-e-2 |
verdrängt das Chat-Modell (Gruppe chat) |
Server einrichten
cd ~/workspace/localAI
cp .env.example .env
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
chmod 600 .env
$EDITOR .env # ALLOWED_NETS, GIDs (getent group render video), UID/GID prüfen
sudo ./scripts/firewall-ufw.sh # einmalig: Port 8800 für LAN + wt0 in ufw öffnen
./scripts/start.sh # prüfen, starten, Modelle vorladen, Verbindungsdaten zeigen
scripts/start.sh legt eine fehlende .env beim ersten Start auch selbst an: mit neuem API-Key, UID/GID und den GIDs von render/video. Weitere Optionen:
| Aufruf | Wirkung |
|---|---|
./scripts/start.sh |
starten, Autocomplete + Embeddings + Hauptmodell vorladen |
./scripts/start.sh --no-preload |
nur starten |
./scripts/start.sh --model glm-4.7-flash |
anderes Chat-Modell vorladen |
./scripts/start.sh --show-key |
API-Key im Klartext anzeigen (für Client-Setup) |
./scripts/start.sh --pull |
Basis-Images aktualisieren und eigenes Image neu bauen |
Nach einem Reboot startet der Stack von selbst: Der Docker-Dienst ist aktiviert, und die Container haben restart: unless-stopped. Autocomplete und Embeddings lädt llama-swap per hooks.on_startup.preload ebenfalls von selbst, das Chat-Modell kommt beim ersten Request dazu.
Der erste Aufruf eines Modells lädt es von Hugging Face herunter (Hauptmodell ~22 GB). Danach läuft alles offline.
Whisper und das Bildmodell lädt llama-swap nicht selbst herunter (~12 GB):
mkdir -p models/whisper models/sd
HF=https://huggingface.co
curl -L -o models/whisper/ggml-large-v3-turbo-q8_0.bin $HF/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q8_0.bin
curl -L -o models/sd/z_image_turbo-Q8_0.gguf $HF/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q8_0.gguf
curl -L -o models/sd/ae.safetensors $HF/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors
curl -L -o models/sd/Qwen3-4B-Instruct-2507-Q8_0.gguf $HF/unsloth/Qwen3-4B-Instruct-2507-GGUF/resolve/main/Qwen3-4B-Instruct-2507-Q8_0.gguf
Warum ein eigenes Image?
whisper-server und sd-server gibt es nur im „unified“-Image von llama-swap, und das für AMD nur als Vulkan-Build. Unter Vulkan ist die Prompt-Verarbeitung von llama.cpp auf der RX 6950 XT aber nur halb so schnell wie unter ROCm (~555 statt ~1030 t/s). llama-swap/Dockerfile nimmt deshalb das ROCm-Image als Basis und kopiert die beiden Server aus dem unified-Image dazu. docker compose up -d --build bzw. scripts/start.sh bauen es automatisch. Beide Tags stehen in .env (LLAMA_SWAP_TAG, UNIFIED_TAG). Bei einem Update beide auf dieselbe llama-swap-Version setzen und ./scripts/start.sh --pull ausführen.
Vulkan zählt die iGPU des Ryzen mit. VK_VISIBLE_DEVICES=1 in .env wählt die RX 6950 XT.
Warum Caddy im Host-Netz?
Ports, die Docker veröffentlicht, umgehen ufw. Deshalb ist llama-swap nur an 127.0.0.1 gebunden. Caddy läuft mit network_mode: host, dort greift ufw, und Caddy sieht die echte Quell-IP. Daraus ergeben sich zwei Schutzschichten:
- ufw lässt Port 8800 nur aus dem LAN und über
wt0zu. - Caddy prüft zusätzlich die Quell-IP (
ALLOWED_NETS) und den Bearer-Token (LLM_API_KEY).
Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard verschlüsselt den Tunnel), im Heim-LAN ist es akzeptabel.
Clients einrichten
- opencode (Terminal-Agent): docs/opencode.md
- Continue (VS-Code-Plugin): docs/continue.md
- Laravel (laravel/ai – Agent, Spracheingabe, Bilder): docs/laravel.md
Schnelltest von jedem Client:
curl -H "Authorization: Bearer <KEY>" http://<server>:8800/v1/models
Betrieb
docker compose ps # Status
docker compose logs -f llama-swap # Modell-Laden, Requests
docker compose logs -f caddy # Zugriffe
docker compose restart # nach Änderungen an .env / Caddyfile
docker compose up -d --build # nach Änderungen an llama-swap/Dockerfile oder den Image-Tags
llama-swap/config.yamlwird automatisch neu geladen, ein Neustart ist nicht nötig.- Web-Oberfläche von llama-swap (Modellstatus, Logs):
http://127.0.0.1:8080/ui, nur lokal auf dem Server erreichbar. - VRAM-Auslastung:
rocm-smi --showmeminfo vram
Modell hinzufügen
Einen Eintrag in llama-swap/config.yaml anlegen, z. B.
mein-modell:
cmd: ${server} ${agent-ctx} -hf <hf-user>/<repo>-GGUF:Q4_K_M
ttl: 3600
Den Eintrag unter routing.router.settings.groups.chat.members aufnehmen. Danach die Client-Configs (client/opencode.json, client/continue/config.yaml) um die neue Modell-ID ergänzen.
GPU-Layer und MoE-Auslagerung in den RAM verteilt llama.cpp automatisch (--fit, Standard: an). --fit-target 3072 hält ~3 GB VRAM für das Autocomplete-Modell frei.
Auf einen anderen PC übertragen
-
Den Ordner kopieren.
models/kann man mitnehmen, um den Download zu sparen. -
.envanpassen:ALLOWED_NETS(LAN-Subnetz, NetBird-Netz),RENDER_GID/VIDEO_GID,HOST_UID/HOST_GID. -
Je nach GPU das Image in
.envwählen (LLAMA_SWAP_TAG). Das eigene Image (llama-swap/Dockerfile) ist für ROCm gebaut. Bei NVIDIA oder Vulkan nimmt man einfacher direkt das passendeunified-…-Image (enthält whisper-/sd-server schon) und ersetztbuild:indocker-compose.ymldurchimage: ghcr.io/mostlygeek/llama-swap:unified-….VK_VISIBLE_DEVICESan die Geräte-Reihenfolge anpassen.GPU Tag-Variante AMD (ROCm) vXXX-rocm-bYYYY(wie hier)NVIDIA vXXX-cuda-bYYYY. Dann indocker-compose.ymldevices/group_add/security_optdurchdeploy.resources.reservations.devices(nvidia) ersetzen.herstellerunabhängig vXXX-vulkan-bYYYY(nur/dev/drinötig)nur CPU vXXX-cpu-bYYYYVerfügbare Tags: https://github.com/mostlygeek/llama-swap/pkgs/container/llama-swap
-
Threads (
--threadsim Makroserver) an die CPU anpassen. -
Firewall des Ziel-PCs entsprechend
scripts/firewall-ufw.shkonfigurieren.
Dateien
| Datei | Inhalt |
|---|---|
docker-compose.yml |
Dienste llama-swap + caddy |
llama-swap/Dockerfile |
ROCm-Image + whisper-server/sd-server |
.env / .env.example |
Secrets, Netze, Ports, GIDs, Image-Tags |
llama-swap/config.yaml |
Modelle, Startparameter, Gruppen |
caddy/Caddyfile |
IP-Filter, API-Key, Reverse Proxy |
client/opencode.json |
Vorlage für opencode-Clients |
client/continue/ |
Vorlage für Continue-Clients |
docs/laravel.md |
Anbindung einer Laravel-App (laravel/ai) |
scripts/start.sh |
Start mit Prüfungen, Preload und Statusausgabe |
scripts/firewall-ufw.sh |
ufw-Freigaben |
PLAN.md |
ursprünglicher Umsetzungsplan |
PLAN-multimodal.md |
Plan + Messwerte für Transkription/Bilder |