# Server UI — architektura uniwersalnego panelu (research DevOps) **Data:** 2026-07-04 **Host:** gmktec-k11, Ubuntu 26.04, RTX 3090 Ti **Kontekst:** Czy UI gpu-fan można przenieść do Server UI w Dockerze, skoro daemon NVML musi zostać na hoście (root)? Jak zbudować panel w stylu appliance (Proxmox / TrueNAS) na Ubuntu? **Powiązane:** - [`stacks/server-ui/`](../stacks/server-ui/) - [`stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md`](../stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md) - [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md) - [`ADR-001-host-agent-control-plane.md`](ADR-001-host-agent-control-plane.md) --- ## 1. Executive summary **Werdykt:** Przeniesienie **UI** gpu-fan do Server UI przy **daemonie NVML na hoście** to **właściwy, branżowo spójny kierunek** (wzorzec *control plane UI + host agent*). **Nie** pakować sterowania wentylatorami do kontenera Docker. **Rekomendowana strategia:** **C** — rozdziel gpu-fan na agent localhost (`127.0.0.1:18090`) + zintegrowany panel w Server UI + dockerizacja Server UI z `docker.sock` i proxy do agenta. **Stan dziś:** Oba panele (gpu-fan :8090, server-ui :8091) działają jako **systemd na hoście**, nie w Dockerze. Cel „wszystkie UI serwerowe w Dockerze” wymaga najpierw dockerizacji server-ui, potem integracji gpu-fan jako host agent. --- ## 2. Pytanie biznesowe > gpu-fan musi działać na hoście (root, NVML). Czy UI gpu-fan może działać w kontenerze Docker razem z server-ui, żeby wszystkie UI serwerowe zarządzały rootem z jednego miejsca? **Odpowiedź skrócona:** | Warstwa | Gdzie | Dlaczego | |---------|-------|----------| | Pętla NVML + zapis wentylatorów | **Host, systemd, root** | Jedyny wspierany i bezpieczny model ([gpu-fan report](../stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md)) | | API agenta (status, krzywa, tryb) | **Host, localhost only** | Most dla UI bez publicznego NVML | | Web UI (dashboard, wykres, przyciski) | **Server UI (docelowo Docker)** | Jedna przeglądarka, jeden port, jeden API key | | Start/stop stacków Docker | **Server UI + docker.sock** | Wzorzec Portainer/Dockge | „UI w Dockerze zarządza rootem” = **proxy HTTP do host agenta**, nie uruchomienie NVML write w kontenerze. --- ## 3. Stan obecny (as-is) ```mermaid flowchart LR browser1["Przeglądarka"] browser2["Przeglądarka"] gpuFan["gpu-fan.service root :8090"] serverUI["server-ui systemd :8091"] dockerStacks["Docker stacks"] gpu["GPU NVML"] browser1 --> gpuFan browser2 --> serverUI gpuFan --> gpu serverUI -->|"docker compose CLI"| dockerStacks serverUI -.->|"link :8090"| gpuFan ``` | Komponent | Runtime | Port | Uprawnienia | Pliki kluczowe | |-----------|---------|------|-------------|----------------| | gpu-fan | systemd, `User=root` | 8090 | NVML write + FastAPI + static UI w **jednym** procesie | `stacks/gpu-fan/app.py`, `fan_controller.py` | | server-ui | systemd | 8091 | `docker compose` subprocess, `nvidia-smi` read-only | `stacks/server-ui/app.py`, `compose_runner.py` | | comfyui / localai / vllm | Docker | 8188 / 8070 / 8000 | GPU compute | `stacks/*/compose.yaml` | | npmplus | Docker, `network_mode: host` | 80, 443, 81 | reverse proxy | `stacks/npmplus/compose.yaml` | ### Problemy architektoniczne 1. **Dwa panele** — porty 8090 i 8091, osobne API key, rozproszony UX. 2. **Coupling UI+daemon w gpu-fan** — `app.py` uruchamia wątek NVML i uvicorn w jednym procesie; static HTML woła `/api/*` na tym samym origin. 3. **Server-ui nie jest w Dockerze** — mimo nazwy „Docker UI” to FastAPI na hoście (`/opt/server-ui`, `server-ui.service`). 4. **Brak jednego entry pointu** — NPMPlus domyślnie nie proxyuje 8090/8091; użytkownik musi znać wiele URL. 5. **„Zarządzanie rootem”** — server-ui nie może bezpośrednio pisać do NVML; potrzebuje host agent API. ### Wymuszenie root w gpu-fan ```python # stacks/gpu-fan/app.py (linie 161–164) if os.geteuid() != 0 and not DRY_RUN: log.error("GPU fan control requires root (NVML write access). Run with sudo.") sys.exit(1) ``` --- ## 4. Analiza coupling gpu-fan (UI + daemon) ### Obecna struktura procesu ``` gpu-fan.service (root) └── python app.py ├── FanController.run_loop() # wątek daemon — co 2s NVML └── uvicorn :8090 ├── GET / → static/index.html (~750 linii JS) ├── GET /api/status ├── GET/PUT /api/curve ├── POST /api/mode └── POST /api/reload ``` Frontend (`static/index.html`) używa **względnych** ścieżek (`fetch('/api/status')`) — działa tylko gdy UI i API są na tym samym hoście:porcie. ### Co można rozdzielić bez ryzyka | Element | Zostaje na hoście | Może do Server UI | |---------|-------------------|-------------------| | `fan_controller.py` pętla NVML | **Tak** | Nie | | FastAPI endpoints `/api/*` | **Tak** (localhost) | Proxy z server-ui | | `static/index.html` | Nie | **Tak** | | Graceful shutdown → auto fan | **Tak** (SIGTERM w systemd) | Nie | ### Docelowy podział (agent) ``` gpu-fan-daemon.service (root) └── python fan_daemon.py # lub app.py --api-only ├── FanController.run_loop() └── uvicorn 127.0.0.1:18090 └── /api/* only (bez static/) server-ui (Docker lub systemd) └── static + proxy /api/gpu-fan/* → http://host.docker.internal:18090/api/* ``` **Krytyczne:** Pętla NVML musi działać **niezależnie** od UI — pad panelu nie może zatrzymać chłodzenia. --- ## 5. Porównanie branżowe ### Proxmox VE (Debian appliance) | Aspekt | Implementacja | Lekcja | |--------|---------------|--------| | Web UI | `pveproxy` na hoście | UI oddzielone od workloadów, ale **nie** w „zwykłym” kontenerze aplikacyjnym | | Operacje VM/storage | `pvedaemon`, QEMU, LVM na hoście | Privileged zawsze na host kernel | | Model | Single-node hypervisor | Jeden panel, wiele usług backendowych na hoście | Proxmox **nie** pakuje hypervisora do Docker — control plane jest częścią OS. ### TrueNAS | Wariant | UI | Storage / hardware | |---------|-----|------------------| | CORE | nginx + middleware na hoście | ZFS w kernelu | | SCALE | UI w middleware (K8s) | Dane nadal przy host storage stack | Lekcja: nawet przy „nowoczesnym” SCALE ciężkie operacje zostają przy hoście. ### Cockpit (Ubuntu) - `cockpit.socket` + moduły na hoście - Dostęp do `systemd`, sieci, storage przez D-Bus / polkit - Oficjalny wzorzec Canonical dla headless Ubuntu Alternatywa: zamiast własnego server-ui — Cockpit + plugin. Minus: brak whitelist stacków AI, polityki GPU, custom NVML fan. ### Portainer / Dockge (homelab Docker) | Element | Wzorzec | |---------|---------| | UI | Kontener | | Docker API | Mount `/var/run/docker.sock` | | Host hardware (fan, disk, systemd) | **Poza scope** — nie sterują NVML | To jest **najbliższy analog** do waszej wizji dla warstwy Docker; gpu-fan wymaga **dodatkowego** host agent (jak brakujący moduł Portainera). ### Macierz porównawcza | Produkt | UI w kontenerze? | Host privileged ops | Pasuje do gmktec-k11? | |---------|------------------|----------------------|------------------------| | Proxmox | Nie (host) | Tak | Wzorzec appliance, za ciężki | | TrueNAS | Częściowo | Tak | Storage-first, nie AI | | Cockpit | Nie | Tak | Generyczny, słaba integracja AI stacks | | Portainer | Tak | Tylko docker.sock | Brak gpu-fan | | **Server UI (propozycja)** | **Tak (docelowo)** | **Via host agent** | **Dopasowany** | --- ## 6. Macierz strategii (A–E) ### A — Status quo (dwa panele, host systemd) - **Plus:** Działa dziś, zero refactoru - **Minus:** 8090 + 8091, dwa klucze, rozproszony UX - **Verdict:** Utrzymanie krótkoterminowe ### B — Split gpu-fan + UI w server-ui, oba systemd na hoście - **Plus:** Jeden port 8091, mniejszy refactor niż Docker - **Minus:** Server-ui nadal nie w kontenerze - **Verdict:** **Dobry krok pośredni** (Faza 1–2 bez Fazy 3) ### C — Split gpu-fan daemon + server-ui w Dockerze (REKOMENDOWANE) - **Plus:** Portainer pattern; jeden panel; agent tylko localhost; :8090 znika z LAN - **Minus:** Dockerfile, mounty, `host.docker.internal` na Linux - **Verdict:** **Strategia docelowa** ### D — Cockpit zamiast server-ui - **Plus:** Dojrzały ekosystem Ubuntu - **Minus:** Custom plugin NVML, brak polityki GPU / whitelist compose - **Verdict:** Tylko przy rezygnacji z własnego UI ### E — gpu-fan NVML w privileged kontenerze - **Plus:** Literalnie „wszystko w Dockerze” - **Minus:** Kruche, `Insufficient Permissions` na GeForce, ryzyko manual fan po crash - **Verdict:** **Odrzuć** — patrz [`gpu-fan/docs/05-DLACZEGO-NIE-DOCKER.md`](../stacks/gpu-fan/docs/05-DLACZEGO-NIE-DOCKER.md) **Wybór:** **C**, z opcjonalnym **B** jako pierwsze PR (integracja UI przed dockerizacją). --- ## 7. Architektura docelowa (to-be) ```mermaid flowchart TB browser["Przeglądarka"] npm["NPMPlus :443 opcjonalnie"] subgraph dockerPlane [Docker control plane] serverUI["server-ui container :8091"] end subgraph hostAgents [Host systemd root] gpuAgent["gpu-fan-daemon 127.0.0.1:18090"] futureAgent["host-agent przyszłość"] end subgraph workloads [Docker workloads] comfyui["comfyui"] localai["localai"] npmplus["npmplus host network"] end gpu["RTX 3090 Ti"] browser --> npm npm --> serverUI browser --> serverUI serverUI -->|"docker.sock"| workloads serverUI -->|"proxy /api/gpu-fan"| gpuAgent serverUI -.->|"future /api/host"| futureAgent gpuAgent --> gpu ``` ### Warstwy odpowiedzialności | Warstwa | Odpowiedzialność | Technologia | |---------|------------------|-------------| | **Presentation** | Jeden dashboard, auth, nawigacja | server-ui | | **Orchestration** | start/stop compose, logi, polityka GPU | server-ui + `docker.sock` | | **Host agents** | NVML fan; przyszłe: systemd, `/data` health | systemd, API localhost | | **Workloads** | AI inference / image gen | istniejące stacki compose | ### Co NIE wchodzi do Server UI (scope) - Pełne osadzenie ComfyUI / LocalAI SPA w iframe — opcjonalne; dziś wystarczą linki `:8188`, `:8070` - Zastąpienie NPMPlus — osobny stack reverse proxy - Pełny hypervisor (VM, ZFS) — poza zakresem ubuntu-bare-metal --- ## 8. Plan implementacji (fazy) ### Faza 1 — Rozdziel gpu-fan (host) **Cel:** Agent API bez publicznego UI. | Zadanie | Pliki | |---------|-------| | Wydziel tryb API-only lub `fan_daemon.py` | `stacks/gpu-fan/app.py` lub nowy moduł | | Env: `GPU_FAN_API_HOST=127.0.0.1`, `GPU_FAN_API_PORT=18090` | `.env.example`, `/opt/control-plane/.env` | | Usuń serwowanie `static/` z produkcji | `app.py` | | Zaktualizuj `gpu-fan.service` | `gpu-fan.service` | | Deprecate publiczny :8090 w `enable-lan.sh` | `scripts/enable-lan.sh` | | Dokumentacja | `docs/`, `README.md` | **Weryfikacja:** ```bash curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: $KEY" ss -tlnp | grep 18090 # 127.0.0.1 only curl -s -o /dev/null -w '%{http_code}\n' http://192.168.100.90:8090/ # connection refused ``` ### Faza 2 — Integracja UI w server-ui | Zadanie | Pliki | |---------|-------| | Przenieś UI gpu-fan do zakładki / podstrony | `server-ui/static/gpu-fan.html` lub sekcja w `index.html` | | Zmień `fetch` na `/api/gpu-fan/...` | JS frontend | | Proxy backend | `server-ui/app.py` — `httpx` lub `requests` do agenta | | Jednolity `API_KEY` dla użytkownika | `.env` server-ui; agent akceptuje ten sam secret z localhost | | Usuń link `:8090` | `server-ui/static/index.html` | **Weryfikacja:** Panel gpu-fan w `:8091`, brak potrzeby otwierania :8090. ### Faza 3 — Dockerize server-ui | Zadanie | Pliki | |---------|-------| | `Dockerfile` | `stacks/server-ui/Dockerfile` | | `compose.yaml` | mount `docker.sock`, repo, env | | `extra_hosts: host.docker.internal:host-gateway` | compose | | Opcja: systemd wrapper `docker compose up -d` | `server-ui.service` lub nowy unit | | Whitelist w `stacks.yaml` — bez zmian | — | **Weryfikacja:** ```bash docker compose --profile server-ui up -d curl -s http://127.0.0.1:8091/api/health curl -s http://127.0.0.1:8091/api/stacks ``` ### Faza 4 — Jeden punkt wejścia (opcjonalnie) - NPMPlus proxy host `panel.` → `127.0.0.1:8091` - UFW: zamknij 18090, opcjonalnie 8091 poza LAN - Patrz BACKLOG P1 — rozdział 08 firewall ### Faza 5 — `host-agent` (przyszłość) Jeden daemon root agregujący: - gpu-fan (NVML) - `systemctl` status wybranych unitów (`gpu-fan`, `server-ui`) - health mountu `/data` - wersje driver / CUDA Server-ui woła `/api/host/*` zamiast wielu portów agentów. --- ## 9. API contract — gpu-fan agent (do proxy) Agent nasłuchuje na `127.0.0.1:18090`. Server-ui mapuje `/api/gpu-fan/{path}` → `/api/{path}`. | Endpoint agenta | Metoda | Opis | Auth | |-----------------|--------|------|------| | `/api/status` | GET | Temperatura, fan speeds, tryb, krzywa | `X-API-Key` | | `/api/curve` | GET | Punkty krzywej | `X-API-Key` | | `/api/curve` | PUT | Zapis krzywej + tryb curve | `X-API-Key` | | `/api/mode` | POST | `{"mode":"auto\|curve\|manual","speed":100}` | `X-API-Key` | | `/api/reload` | POST | Przeładuj `curve.json` | `X-API-Key` | **Bezpieczeństwo agenta:** - Bind tylko `127.0.0.1` (nie `0.0.0.0`) - Akceptuj requesty tylko z loopback lub z zaufanego `GPU_FAN_TRUSTED_PROXY` (server-ui IP w docker bridge — opcjonalnie) - Ten sam `API_KEY` co server-ui lub osobny `AGENT_KEY` w env server-ui (nie w repo) **Przykład proxy (szkic Python):** ```python # server-ui/app.py (szkic — nie zaimplementowano) GPU_FAN_AGENT = os.environ.get("GPU_FAN_AGENT_URL", "http://127.0.0.1:18090") @app.api_route("/api/gpu-fan/{path:path}", methods=["GET", "PUT", "POST"]) async def proxy_gpu_fan(path: str, request: Request): require_mutation_auth(request) # lub require_auth dla GET też url = f"{GPU_FAN_AGENT}/api/{path}" # forward method, body, X-API-Key header ``` --- ## 10. Szkic `compose.yaml` — server-ui (bez implementacji) ```yaml # stacks/server-ui/compose.yaml — PROPOZYCJA, nie wdrożono name: server-ui services: server-ui: build: . container_name: server-ui profiles: [server-ui] restart: unless-stopped ports: - "${SERVER_UI_PORT:-8091}:8091" environment: - SERVER_UI_HOST=0.0.0.0 - SERVER_UI_PORT=8091 - API_KEY=${API_KEY} - REPO_ROOT=/repo - GPU_FAN_AGENT_URL=http://host.docker.internal:18090 volumes: - /var/run/docker.sock:/var/run/docker.sock - ${REPO_ROOT:-/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal}:/repo:ro extra_hosts: - "host.docker.internal:host-gateway" ``` **Uwagi:** - `docker compose` w kontenerze wymaga CLI w obrazie lub wywołania Docker API bezpośrednio (obecnie `compose_runner.py` używa subprocess — działa z mounted sock + `docker` CLI w image). - Ścieżki `compose_dir` w `stacks.yaml` muszą istnieć w `REPO_ROOT` wewnątrz kontenera (`/repo/stacks/...`). --- ## 11. Ryzyka i mitigacje | Ryzyko | Prawdopodobieństwo | Mitigacja | |--------|-------------------|-----------| | Pad server-ui = brak edycji krzywej | Średnie | Daemon gpu-fan **niezależny** — ostatnia krzywa dalej działa | | Server-ui w Docker nie widzi ścieżek compose | Wysokie przy złym mount | Mount całego repo; test `REPO_ROOT` w healthcheck | | `host.docker.internal` niedostępny | Niskie na Docker 20.10+ | `extra_hosts: host-gateway` | | Dwa API key — UX | Średnie | Jeden klucz w UI; proxy dokleja secret do agenta | | Regresja graceful shutdown fan | Niskie przy zachowaniu systemd | Nie ruszać `fan_controller.shutdown()`; test po `systemctl stop` | | npmplus + server-ui port conflict | Brak | 8091 vs 80/443/81 | --- ## 12. Anti-patterns (czego unikać) 1. **NVML fan control w kontenerze Docker** — odrzucone w gpu-fan research. 2. **Jeden monolityczny proces root z UI na 0.0.0.0** — obecny gpu-fan; do refaktoru. 3. **Server-ui bez docker.sock a z pretensją do zarządzania kontenerami** — wymaga sock lub Docker API. 4. **Osadzanie ComfyUI w server-ui zamiast linków** — duży scope, CORS, osobne sesje. 5. **Budowa „Proxmox w Dockerze”** — over-engineering dla single-node AI homelab. 6. **Publiczny port agenta 18090** — tylko localhost. --- ## 13. Autentykacja — model docelowy | Warstwa | Mechanizm | |---------|-----------| | Użytkownik → server-ui | `X-API-Key` (mutacje + opcjonalnie odczyt gpu-fan) | | server-ui → gpu-fan agent | Ten sam key lub `AGENT_KEY` w env server-ui; request z kontenera przez `host.docker.internal` | | Użytkownik → ComfyUI/LocalAI | Osobne klucze aplikacji (poza server-ui) | | NPMPlus → server-ui | TLS + opcjonalnie basic auth na proxy host | Docelowo: **jeden panel, jeden klucz** do operacji serwerowych (start/stop stacków + gpu-fan). --- ## 14. Checklist dla kolejnego agenta ### Research (ten dokument) - [x] Raport SERVER-UI-ARCHITECTURE-RESEARCH.md - [x] ADR-001-host-agent-control-plane.md - [x] Link w BACKLOG.md ### Implementacja (przyszłe PR) - [ ] Faza 1: gpu-fan API-only na localhost:18090 - [ ] Faza 2: proxy + UI w server-ui - [ ] Faza 3: Dockerfile + compose server-ui - [ ] Faza 4: NPMPlus proxy panel - [ ] Faza 5: host-agent (opcjonalnie) --- ## 15. Podsumowanie jednym akapitem Przeniesienie **samego UI** gpu-fan do Server UI w Dockerze, przy **daemonie NVML na hoście**, to właściwy plan zgodny z Portainer (UI w kontenerze) i Proxmox (privileged na hoście). Najpierw rozdziel proces gpu-fan na agent localhost i panel, potem dockerizuj server-ui z `docker.sock` i proxy do agenta; w dłuższej perspektywie rozważ jeden `host-agent` zamiast wielu portów. **Nie** przenoś NVML do kontenera.