diff --git a/.env.example b/.env.example index fbaeb39..4168b23 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,12 @@ ALLOWED_NETS="127.0.0.0/8 ::1 192.168.178.0/24 100.73.0.0/16" LISTEN_PORT=8800 BACKEND_PORT=8080 +# Web-Chat (Open WebUI): Port nach außen (Caddy) und intern (nur 127.0.0.1) +WEBUI_PORT=8801 +WEBUI_BACKEND_PORT=3000 +# Schlüssel für Login-Sitzungen: openssl rand -hex 32 +WEBUI_SECRET_KEY=CHANGE_ME + # Host-Nutzer, dem die Modelldateien in ./models gehören: id -u / id -g HOST_UID=1000 HOST_GID=1000 @@ -33,3 +39,4 @@ LLAMA_SWAP_TAG=v265-rocm-b11610 # Quelle für whisper-server/sd-server (gleiche llama-swap-Version wie oben wählen) UNIFIED_TAG=unified-vulkan-265 CADDY_TAG=2.10.2-alpine +OPEN_WEBUI_TAG=v0.11.4 diff --git a/PLAN-cluster.md b/PLAN-cluster.md new file mode 100644 index 0000000..184f01b --- /dev/null +++ b/PLAN-cluster.md @@ -0,0 +1,72 @@ +# Plan: NVIDIA-Windows-Rechner als flexibler Cluster hinter dem bestehenden Gateway + +## Context +Mehrere Windows-PCs mit NVIDIA-GPUs sollen zusätzliche Rechenleistung liefern. Der bestehende +Linux-PC (AMD, Docker: Caddy + llama-swap) bleibt der **einzige Endpunkt** (:8800, gleicher API-Key). +**Jeder Knoten kann jedes Cluster-Modell ausführen** und wechselt bei Bedarf (llama-swap). Linux ist +auf den Knoten nicht nötig: llama-swap (`llama-swap_265_windows_amd64.zip`) und llama.cpp +(`llama-bXXXXX-bin-win-cuda-{12.4|13.4}-x64.zip` + `cudart-…`) gibt es fertig für Windows. + +Erkenntnisse aus der Recherche: +- llama-swap v265 leitet über `peers` pro Modell an **einen** Server weiter (URL-Pfad wird angehängt). Das Load-Balancing übernimmt deshalb Caddy. +- llama-swap löst `${env.VAR}` in der Config auf, also kommen keine Secrets ins Git. +- Ältere llama.cpp-Builds werden gelöscht (b11610 gibt es schon nicht mehr). Der Installer wählt den Build über die GitHub-API (`latest` oder fest). +- Caddy auf dem Host-Netz ist für den Container wegen ufw schwer erreichbar. Deshalb läuft der Loadbalancer als **eigener Container im Compose-Netz**. + +## Architektur +``` +Client ─► Caddy :8800 (IP-Filter, API-Key) ─► llama-swap (AMD, lokal) + │ lokale Modelle wie bisher + └─ peers: cluster- ─► http://cluster-lb:8090/m/cluster- +cluster-lb (Caddy-Container, generiert): + /m/cluster-qwen3.5-35b-a3b/* ─► Knoten [A, B, C] (Stammknoten A) + /m/cluster-glm-4.7-flash/* ─► Knoten [B, C, A] (Stammknoten B) + /m/cluster-qwen3.5-9b/* ─► Knoten [C, A, B] (Stammknoten C) +Win-Knoten: llama-swap.exe :8080 mit ALLEN cluster-Modellen, API-Key, Firewall nur Gateway-IP +``` +**Routing ohne unnötige Modellwechsel:** Für jedes Modell enthält der Pool alle Knoten, aber in rotierter Reihenfolge. Dazu kommen `lb_policy first` und `unhealthy_request_count `: +- Eine Anfrage geht an den Stammknoten des Modells. Dort ist das Modell meist schon geladen und der Prompt-Cache noch vorhanden. +- Ist der Stammknoten belegt oder ausgefallen, geht sie an den nächsten Knoten. Der lädt das Modell dann, das ist der flexible Fall. +- Sind alle Knoten belegt, wartet Caddy (`lb_try_duration 5m`, `lb_try_interval 1s`), bis einer frei wird. +- Health-Checks auf `/health` alle 10 s, ausgefallene Knoten werden übersprungen. +- Wenn es mehr Modelle als Knoten gibt, teilen sich Modelle einen Stammknoten (Zuordnung reihum). + +## Änderungen auf dem Gateway (Linux) +1. **[docker-compose.yml](docker-compose.yml)**: neuer Service `cluster-lb` (`caddy:${CADDY_TAG}`), nur im Compose-Netz und ohne Port nach außen. Er mountet `caddy/cluster.Caddyfile`. `llama-swap` bekommt `CLUSTER_API_KEY` als Umgebungsvariable. +2. **Neu `scripts/cluster-gen.sh`**: liest `CLUSTER_NODES="192.168.178.50:8080 192.168.178.51:8080 …"` und `CLUSTER_SLOTS` (Standard 1) aus `.env`. Die Modellliste kommt aus `cluster/windows/node-config.yaml` (einzige Quelle, Schlüssel `cluster-*`). Daraus erzeugt das Skript `caddy/cluster.Caddyfile` (in `.gitignore`), mit einem `handle_path /m//*` und rotierter Upstream-Liste pro Modell. Ohne Knoten entsteht eine gültige Datei, die 503 liefert. [scripts/start.sh](scripts/start.sh) ruft das Skript vor `docker compose up` auf. +3. **[llama-swap/config.yaml](llama-swap/config.yaml)**: Block `peers:` mit einem Eintrag pro Cluster-Modell, `proxy: http://cluster-lb:8090/m/`, `apiKey: ${env.CLUSTER_API_KEY}`, dazu großzügige `timeouts.responseHeader` für Modellwechsel und Download. +4. **`.env.example` / `.env`**: `CLUSTER_API_KEY` (start.sh erzeugt ihn wie `LLM_API_KEY`, wenn er fehlt), `CLUSTER_NODES`, `CLUSTER_SLOTS`. +5. **Neu `scripts/cluster-status.sh`**: zeigt pro Knoten `/health`, das geladene Modell (`/running`) und die Antwortzeit. start.sh zeigt die Ausgabe im Statusblock, wenn Knoten gesetzt sind. + +## Windows-Knoten-Skripte (`cluster/windows/`) +- **`install-node.ps1`** (als Admin; wiederholbar, dient auch zum Update): + Parameter `-GatewayIP` und `-ApiKey` (Pflicht), `-Port 8080`, `-InstallDir C:\localai-node`, `-LlamaBuild latest`, `-Cuda auto|12.4|13.4`, `-Preload ` (optional), `-HfToken`, `-KeepSleep`, `-Force`. + 1. Prüft Admin-Rechte, `nvidia-smi` (GPU, VRAM, Compute Capability, Treiber) und freien Speicherplatz (Warnung unter 60 GB, weil alle Modelle auf jedem Knoten liegen können). + 2. CUDA-Variante: 13.4, wenn Compute Capability ≥ 7.5 und Treiber ≥ 580, sonst 12.4. + 3. Lädt llama-swap und llama.cpp CUDA + cudart herunter, entpackt sie nach `bin\` und testet `llama-server.exe --version`. + 4. Schreibt `config.yaml` aus der Vorlage. Bestehende Dateien bleiben unangetastet, außer bei `-Force`. + 5. Erzeugt `start-node.cmd`: setzt `LLAMA_CACHE`, `HF_TOKEN` und `NODE_API_KEY`, startet `llama-swap.exe -config … -listen 0.0.0.0:PORT` und schreibt das Log nach `logs\`. + 6. Firewall-Regel: TCP-Port eingehend nur von `GatewayIP`. + 7. Geplante Aufgabe `LocalAI-Node`: Start beim Booten, als SYSTEM, ohne Laufzeitlimit, Neustart bei Absturz. + 8. Standby am Netzteil aus (`powercfg`), außer bei `-KeepSleep`. + 9. Startet die Aufgabe, wartet auf `/health` und gibt die Zeile für `CLUSTER_NODES` aus. +- **`download-models.ps1`** (optional): lädt alle Cluster-Modelle vorab herunter, indem es jedes Modell einmal kurz anfragt. So dauert die erste echte Anfrage nicht wegen eines 20-GB-Downloads. +- **`node-config.yaml`** (Vorlage, Quelle der Modellliste): alle Cluster-Chat-Modelle (`cluster-qwen3.5-35b-a3b`, `cluster-glm-4.7-flash`, `cluster-qwen3.5-9b`) in **einer** Swap-Gruppe, Pfade mit `/`, `--fit` mit `--fit-target 1024` (passt sich automatisch an den VRAM des Knotens an), `-np `. Dazu `apiKeys: ["${env.NODE_API_KEY}"]`, optional Preload, `healthCheckTimeout: 3600`. +- **`install-node.cmd`**: Doppelklick-Wrapper, fordert Admin-Rechte an, setzt `-ExecutionPolicy Bypass` und fragt Gateway-IP und Key ab. +- **`status-node.ps1`**: Zustand der Aufgabe, `/health`, `/running`, `nvidia-smi`, letzte Logzeilen. +- **`uninstall-node.ps1`**: entfernt Aufgabe und Firewall-Regel und löscht optional das Verzeichnis (`-KeepModels`). + +## Doku +- Neu **`docs/cluster.md`**: Architektur und Routing (Stammknoten, Überlauf), Ablauf (Key → Knoten installieren → IP in `CLUSTER_NODES` → `./scripts/start.sh`), Modell hinzufügen (nur in `node-config.yaml` + `peers`, dann Knoten mit `-Force` aktualisieren). Grenzen: Ein Modell, das größer als VRAM+RAM eines Knotens ist, wird nicht über mehrere PCs verteilt. Kleine GPUs lagern per `--fit` in den RAM aus und sind dann langsamer. +- **[README.md](README.md)**: Abschnitt „Cluster“ mit Link, Dateiliste ergänzen. Die Client-Doku nennt die `cluster-*`-Modellnamen. + +## Verifikation +1. `cluster-gen.sh` mit 0, 1 und 3 Test-Knoten ausführen und jeweils `caddy validate` (Docker, `caddy:2.10.2-alpine`) laufen lassen. Außerdem `docker compose config`. +2. Kette ohne Windows testen: Als Fake-Knoten dient eine zweite llama-swap-Linux-Binary bzw. ein Echo-Server im Scratchpad, auf zwei Ports. Prüfen: + - `/v1/models` zeigt `cluster-*`. + - Die Anfrage läuft über :8800 → peer → cluster-lb → Stammknoten. + - Streaming funktioniert. + - Bei belegtem Stammknoten geht die Anfrage an den nächsten Knoten. + - Ein gestoppter Knoten wird übersprungen. +3. PowerShell-Syntax mit `pwsh` parsen, falls verfügbar. Den echten Lauf auf einem Windows-PC macht der Nutzer; der Installer endet mit einem Selbsttest (`/health`). +4. Der bestehende Betrieb bleibt unverändert: `./scripts/start.sh` startet weiter erfolgreich, auch ohne Knoten. diff --git a/README.md b/README.md index d86ed7a..faed177 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# LocalAI – zentraler LLM-Server für opencode, Continue & Laravel +# LocalAI – zentraler LLM-Server für opencode, Continue, Laravel & Web-Chat 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 +Client-PC (opencode / VS Code + Continue / Browser) ──LAN / NetBird──┐ + ▼ + caddy (Host-Netz) :8800 API: IP-Filter + API-Key :8801 Web-Chat: IP-Filter + ├─► open-webui (nur 127.0.0.1:3000) Web-Chat mit eigenem Login ──┐ + └─► 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 @@ -31,11 +32,11 @@ 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 +sudo ./scripts/firewall-ufw.sh # einmalig: Ports 8800 + 8801 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: +`scripts/start.sh` legt eine fehlende `.env` beim ersten Start auch selbst an: mit neuem API-Key, WebUI-Schlüssel, UID/GID und den GIDs von render/video. Weitere Optionen: | Aufruf | Wirkung | |---|---| @@ -66,8 +67,8 @@ Vulkan zählt die iGPU des Ryzen mit. `VK_VISIBLE_DEVICES=1` in `.env` wählt di ### 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: -1. **ufw** lässt Port 8800 nur aus dem LAN und über `wt0` zu. -2. **Caddy** prüft zusätzlich die Quell-IP (`ALLOWED_NETS`) und den Bearer-Token (`LLM_API_KEY`). +1. **ufw** lässt die Ports 8800 (API) und 8801 (Web-Chat) nur aus dem LAN und über `wt0` zu. +2. **Caddy** prüft zusätzlich die Quell-IP (`ALLOWED_NETS`), bei der API außerdem den Bearer-Token (`LLM_API_KEY`). Beim Web-Chat übernimmt der Login von Open WebUI diese Rolle. Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard verschlüsselt den Tunnel), im Heim-LAN ist es akzeptabel. @@ -76,6 +77,7 @@ Die API läuft über plain HTTP. Über NetBird ist das unkritisch (WireGuard ver - **opencode** (Terminal-Agent): [docs/opencode.md](docs/opencode.md) - **Continue** (VS-Code-Plugin): [docs/continue.md](docs/continue.md) - **Laravel** (laravel/ai – Agent, Spracheingabe, Bilder): [docs/laravel.md](docs/laravel.md) +- **Web-Chat** (Open WebUI im Browser, `http://:8801`): [docs/webchat.md](docs/webchat.md) Schnelltest von jedem Client: ```bash @@ -88,6 +90,7 @@ curl -H "Authorization: Bearer " http://:8800/v1/models docker compose ps # Status docker compose logs -f llama-swap # Modell-Laden, Requests docker compose logs -f caddy # Zugriffe +docker compose logs -f open-webui # Web-Chat docker compose restart # nach Änderungen an .env / Caddyfile docker compose up -d --build # nach Änderungen an llama-swap/Dockerfile oder den Image-Tags ``` @@ -127,11 +130,12 @@ GPU-Layer und MoE-Auslagerung in den RAM verteilt llama.cpp automatisch (`--fit` | Datei | Inhalt | |---|---| -| `docker-compose.yml` | Dienste llama-swap + caddy | +| `docker-compose.yml` | Dienste llama-swap, open-webui, 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 | +| `caddy/Caddyfile` | IP-Filter, API-Key, Reverse Proxy (API + Web-Chat) | +| `docs/webchat.md` | Web-Chat (Open WebUI) | | `client/opencode.json` | Vorlage für opencode-Clients | | `client/continue/` | Vorlage für Continue-Clients | | `docs/laravel.md` | Anbindung einer Laravel-App (laravel/ai) | diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 5ff0aec..5a53046 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -20,3 +20,16 @@ } } } + +# Web-Chat (Open WebUI): nur erlaubte Netze, Login übernimmt Open WebUI selbst +:{$WEBUI_PORT} { + @blocked not remote_ip {$ALLOWED_NETS} + respond @blocked "forbidden" 403 + + reverse_proxy 127.0.0.1:{$WEBUI_BACKEND_PORT} { + flush_interval -1 + transport http { + read_timeout 30m + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml index c165487..c955e36 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ # Lokaler LLM-Server: llama-swap (llama.cpp/ROCm + whisper/sd-server) hinter Caddy (IP-Filter + API-Key) +# Web-Chat: Open WebUI, ebenfalls hinter Caddy (IP-Filter + eigener Login) # Start: docker compose up -d --build Logs: docker compose logs -f llama-swap name: localai @@ -51,7 +52,57 @@ services: ALLOWED_NETS: "${ALLOWED_NETS:-127.0.0.0/8 ::1}" LISTEN_PORT: "${LISTEN_PORT:-8800}" BACKEND_PORT: "${BACKEND_PORT:-8080}" + WEBUI_PORT: "${WEBUI_PORT:-8801}" + WEBUI_BACKEND_PORT: "${WEBUI_BACKEND_PORT:-3000}" volumes: - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro depends_on: - llama-swap + - open-webui + + # Web-Chat im Browser. Spricht intern direkt mit llama-swap (Docker-Netz, ohne Caddy). + # Die Werte unten gelten nur beim ersten Start – danach im Admin-Bereich ändern. + open-webui: + image: ghcr.io/open-webui/open-webui:${OPEN_WEBUI_TAG:-v0.11.4} + restart: unless-stopped + # nur lokal erreichbar – von außen ausschließlich über Caddy + ports: + - "127.0.0.1:${WEBUI_BACKEND_PORT:-3000}:8080" + environment: + WEBUI_SECRET_KEY: "${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY in .env setzen}" + # erster Account wird Admin, weitere müssen freigeschaltet werden + DEFAULT_USER_ROLE: pending + ENABLE_VERSION_UPDATE_CHECK: "false" + ENABLE_OLLAMA_API: "false" + OPENAI_API_BASE_URL: http://llama-swap:8080/v1 + OPENAI_API_KEY: none + # nur eine Chat-Sitzung gleichzeitig (-np 1): keine Zusatz-Requests beim Tippen + ENABLE_AUTOCOMPLETE_GENERATION: "false" + ENABLE_FOLLOW_UP_GENERATION: "false" + ENABLE_EVALUATION_ARENA_MODELS: "false" + # Dokumente/RAG: Embeddings von nomic-embed statt eines lokalen Downloads + RAG_EMBEDDING_ENGINE: openai + RAG_EMBEDDING_MODEL: nomic-embed + RAG_OPENAI_API_BASE_URL: http://llama-swap:8080/v1 + RAG_OPENAI_API_KEY: none + RAG_EMBEDDING_MODEL_AUTO_UPDATE: "false" + # Spracheingabe über whisper + AUDIO_STT_ENGINE: openai + AUDIO_STT_MODEL: whisper + AUDIO_STT_OPENAI_API_BASE_URL: http://llama-swap:8080/v1 + AUDIO_STT_OPENAI_API_KEY: none + WHISPER_MODEL_AUTO_UPDATE: "false" + # Bilder über z-image (verdrängt das Chat-Modell, siehe config.yaml) + ENABLE_IMAGE_GENERATION: "true" + IMAGE_GENERATION_ENGINE: openai + IMAGE_GENERATION_MODEL: z-image + IMAGE_SIZE: 1024x1024 + IMAGES_OPENAI_API_BASE_URL: http://llama-swap:8080/v1 + IMAGES_OPENAI_API_KEY: none + volumes: + - open-webui:/app/backend/data + depends_on: + - llama-swap + +volumes: + open-webui: diff --git a/docs/webchat.md b/docs/webchat.md new file mode 100644 index 0000000..8fca930 --- /dev/null +++ b/docs/webchat.md @@ -0,0 +1,49 @@ +# Web-Chat (Open WebUI) + +Chat im Browser, ohne Client-Installation. Open WebUI läuft im selben Stack und spricht intern direkt mit llama-swap. Nach außen liegt es hinter Caddy auf Port **8801** (`WEBUI_PORT`). Dort greifen derselbe IP-Filter (`ALLOWED_NETS`) und ufw wie bei der API. Einen API-Key braucht der Browser nicht, Open WebUI hat einen eigenen Login. + +| Zugang | URL | +|---|---| +| lokal | http://127.0.0.1:8801 | +| LAN | http://192.168.178.72:8801 | +| NetBird | http://alegab-pc.vpn.local:8801 | + +`./scripts/start.sh` zeigt die aktuellen Adressen an. + +## Erster Start + +1. Einmalig den Port in der Firewall öffnen: `sudo ./scripts/firewall-ufw.sh` +2. Die URL öffnen und **sofort einen Account anlegen**. Der erste Account wird Admin. +3. Weitere Accounts landen als „ausstehend“ und müssen unter *Admin-Bereich → Benutzer* freigeschaltet werden. Wer niemanden sonst zulassen will: *Admin-Bereich → Einstellungen → Allgemein → Registrierung* abschalten. + +## Was vorkonfiguriert ist + +| Funktion | Modell | Hinweis | +|---|---|---| +| Chat | `qwen3.5-35b-a3b`, `glm-4.7-flash`, `qwen3.5-9b` | Modellwechsel lädt das Modell in llama-swap um (dauert einige Sekunden) | +| Dokumente / RAG (Datei in den Chat ziehen) | `nomic-embed` | | +| Spracheingabe (Mikrofon) | `whisper` | nur über HTTPS oder `localhost`, siehe unten | +| Bilder (Bild-Knopf im Chat) | `z-image` | verdrängt das Chat-Modell; der nächste Chat lädt es neu | + +Die Auswahlliste zeigt auch `qwen2.5-coder-1.5b`, `nomic-embed`, `whisper` und `z-image`. Diese Modelle taugen nicht zum Chatten. Ausblenden kann man sie unter *Admin-Bereich → Einstellungen → Modelle*. + +Die Chat-Modelle laufen mit `-np 1`, also mit einer Sitzung gleichzeitig. Wenn opencode gerade arbeitet, wartet der Web-Chat (und umgekehrt). Titel-Generierung nutzt dasselbe Modell und hängt sich hinten an. Autocomplete- und Folgefragen-Vorschläge sind deshalb abgeschaltet. + +## Einstellungen ändern + +Die Umgebungsvariablen in `docker-compose.yml` gelten **nur beim allerersten Start**. Danach speichert Open WebUI alles in seiner Datenbank. Spätere Änderungen macht man im *Admin-Bereich → Einstellungen* (Verbindungen, Dokumente, Audio, Bilder). + +Die Daten (Accounts, Chats, Dokumente) liegen im Docker-Volume `localai_open-webui`. +- Sichern: `docker run --rm -v localai_open-webui:/d -v "$PWD":/b alpine tar czf /b/open-webui-backup.tgz -C /d .` +- Komplett zurücksetzen: `docker compose down && docker volume rm localai_open-webui` + +## Spracheingabe von anderen Rechnern + +Browser geben das Mikrofon nur auf HTTPS-Seiten oder `localhost` frei. Über `http://:8801` bleibt der Mikrofon-Knopf daher wirkungslos. Für Chrome/Chromium gibt es eine Ausnahme pro Adresse: +`chrome://flags/#unsafely-treat-insecure-origin-as-secure` → `http://alegab-pc.vpn.local:8801` eintragen, neu starten. + +Dateien hochladen (Audio-Datei transkribieren) funktioniert auch ohne HTTPS. + +## Update + +`OPEN_WEBUI_TAG` in `.env` anheben (Releases: https://github.com/open-webui/open-webui/releases), dann `docker compose up -d`. Open WebUI migriert die Datenbank selbst. Vorher sichern. diff --git a/scripts/firewall-ufw.sh b/scripts/firewall-ufw.sh index f61ed9e..b6ae5a7 100755 --- a/scripts/firewall-ufw.sh +++ b/scripts/firewall-ufw.sh @@ -1,12 +1,15 @@ #!/usr/bin/env bash # Einmalig ausführen: sudo ./scripts/firewall-ufw.sh -# Öffnet den Caddy-Port nur für LAN und NetBird (wt0). Caddy filtert zusätzlich selbst. +# Öffnet die Caddy-Ports (API + Web-Chat) nur für LAN und NetBird (wt0). Caddy filtert zusätzlich selbst. set -euo pipefail PORT="${LISTEN_PORT:-8800}" +WEBUI_PORT="${WEBUI_PORT:-8801}" LAN="${LAN_NET:-192.168.178.0/24}" VPN_IF="${VPN_IF:-wt0}" ufw allow from "$LAN" to any port "$PORT" proto tcp comment 'localai LAN' ufw allow in on "$VPN_IF" to any port "$PORT" proto tcp comment 'localai NetBird' -ufw status numbered | grep -E "$PORT" || true +ufw allow from "$LAN" to any port "$WEBUI_PORT" proto tcp comment 'localai webchat LAN' +ufw allow in on "$VPN_IF" to any port "$WEBUI_PORT" proto tcp comment 'localai webchat NetBird' +ufw status numbered | grep -E "$PORT|$WEBUI_PORT" || true diff --git a/scripts/start.sh b/scripts/start.sh index 9b7e39d..492186e 100755 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -47,6 +47,7 @@ if [[ ! -f .env ]]; then warn ".env fehlt – wird aus .env.example erzeugt" cp .env.example .env sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env + sed -i "s/^WEBUI_SECRET_KEY=.*/WEBUI_SECRET_KEY=$(openssl rand -hex 32)/" .env sed -i "s/^HOST_UID=.*/HOST_UID=$(id -u)/; s/^HOST_GID=.*/HOST_GID=$(id -g)/" .env rg=$(getent group render | cut -d: -f3 || true); vg=$(getent group video | cut -d: -f3 || true) [[ -n "$rg" ]] && sed -i "s/^RENDER_GID=.*/RENDER_GID=$rg/" .env @@ -55,21 +56,30 @@ if [[ ! -f .env ]]; then warn "Bitte ALLOWED_NETS in .env prüfen (LAN-/NetBird-Netz)" fi +# ältere .env ohne Web-Chat-Einträge ergänzen +if ! grep -q '^WEBUI_SECRET_KEY=' .env; then + printf '\nWEBUI_PORT=8801\nWEBUI_BACKEND_PORT=3000\nWEBUI_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" >> .env + warn "Web-Chat-Einträge (WEBUI_*) in .env ergänzt" +fi + set -a; . ./.env; set +a LISTEN_PORT="${LISTEN_PORT:-8800}" BACKEND_PORT="${BACKEND_PORT:-8080}" +WEBUI_PORT="${WEBUI_PORT:-8801}" +WEBUI_BACKEND_PORT="${WEBUI_BACKEND_PORT:-3000}" [[ -n "${LLM_API_KEY:-}" && "$LLM_API_KEY" != "CHANGE_ME" ]] || die "LLM_API_KEY in .env setzen (openssl rand -hex 32)" +[[ -n "${WEBUI_SECRET_KEY:-}" && "$WEBUI_SECRET_KEY" != "CHANGE_ME" ]] || die "WEBUI_SECRET_KEY in .env setzen (openssl rand -hex 32)" ok ".env geladen" # Ports dürfen nur von unserem eigenen Stack belegt sein running=$(docker compose ps --status running -q 2>/dev/null | wc -l) if [[ "$running" -eq 0 ]]; then - for p in "$LISTEN_PORT" "$BACKEND_PORT"; do + for p in "$LISTEN_PORT" "$BACKEND_PORT" "$WEBUI_PORT" "$WEBUI_BACKEND_PORT"; do if ss -Htln "sport = :$p" | grep -q .; then die "Port $p ist bereits belegt: $(ss -Htlnp "sport = :$p" | awk '{print $NF}' | head -1) – in .env ändern" fi done - ok "Ports $LISTEN_PORT und $BACKEND_PORT frei" + ok "Ports $LISTEN_PORT, $BACKEND_PORT, $WEBUI_PORT und $WEBUI_BACKEND_PORT frei" fi if systemctl is-active --quiet ollama 2>/dev/null; then @@ -77,9 +87,11 @@ if systemctl is-active --quiet ollama 2>/dev/null; then fi if command -v ufw >/dev/null && systemctl is-active --quiet ufw 2>/dev/null; then - if [[ -r /etc/ufw/user.rules ]] && ! grep -q "dport $LISTEN_PORT" /etc/ufw/user.rules; then - warn "Keine ufw-Freigabe für Port $LISTEN_PORT gefunden → sudo ./scripts/firewall-ufw.sh" - fi + for p in "$LISTEN_PORT" "$WEBUI_PORT"; do + if [[ -r /etc/ufw/user.rules ]] && ! grep -q "dport $p" /etc/ufw/user.rules; then + warn "Keine ufw-Freigabe für Port $p gefunden → sudo ./scripts/firewall-ufw.sh" + fi + done fi mkdir -p models @@ -113,6 +125,14 @@ code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' -H "Authorization: Bearer $LL [[ "$code" == "200" ]] || die "API mit Key liefert HTTP $code" ok "Caddy bereit (Auth aktiv)" +# Open WebUI braucht beim ersten Start etwas länger (Datenbank anlegen) +for i in $(seq 1 120); do + curl -fsS -m 2 "http://127.0.0.1:$WEBUI_PORT/health" >/dev/null 2>&1 && break + (( i == 120 )) && die "Web-Chat antwortet nicht – docker compose logs open-webui" + sleep 1 +done +ok "Web-Chat bereit" + # ---------------------------------------------------------------- Preload if (( PRELOAD )); then step "Lade Modelle vor (erster Start lädt ggf. von Hugging Face herunter)" @@ -149,14 +169,17 @@ step "Verbindung" lan_ip=$(ip -4 -o addr show scope global | awk '$2 !~ /^(wt|docker|virbr|br-|veth)/ {split($4,a,"/"); print a[1]; exit}') nb_ip=$(ip -4 -o addr show wt0 2>/dev/null | awk '{split($4,a,"/"); print a[1]}') nb_fqdn=$(netbird status 2>/dev/null | awk '/^FQDN:/{print $2}' || true) -echo " lokal: http://127.0.0.1:$LISTEN_PORT/v1" -[[ -n "$lan_ip" ]] && echo " LAN: http://$lan_ip:$LISTEN_PORT/v1" -[[ -n "$nb_ip" ]] && echo " NetBird: http://$nb_ip:$LISTEN_PORT/v1${nb_fqdn:+ (http://$nb_fqdn:$LISTEN_PORT/v1)}" +echo " API lokal: http://127.0.0.1:$LISTEN_PORT/v1" +[[ -n "$lan_ip" ]] && echo " API LAN: http://$lan_ip:$LISTEN_PORT/v1" +[[ -n "$nb_ip" ]] && echo " API NetBird: http://$nb_ip:$LISTEN_PORT/v1${nb_fqdn:+ (http://$nb_fqdn:$LISTEN_PORT/v1)}" +echo " Chat lokal: http://127.0.0.1:$WEBUI_PORT" +[[ -n "$lan_ip" ]] && echo " Chat LAN: http://$lan_ip:$WEBUI_PORT" +[[ -n "$nb_ip" ]] && echo " Chat NetBird: http://$nb_ip:$WEBUI_PORT${nb_fqdn:+ (http://$nb_fqdn:$WEBUI_PORT)}" if (( SHOW_KEY )); then - echo " API-Key: $LLM_API_KEY" + echo " API-Key: $LLM_API_KEY" else - echo " API-Key: ${LLM_API_KEY:0:6}… (vollständig: --show-key)" + echo " API-Key: ${LLM_API_KEY:0:6}… (vollständig: --show-key)" fi -echo " Web-UI: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)" +echo " llama-swap: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)" echo -echo " Clients einrichten: docs/opencode.md, docs/continue.md" +echo " Clients einrichten: docs/opencode.md, docs/continue.md, docs/webchat.md"