Infrastructure configs for GMKtec K11 (Docker, vLLM, LocalAI, ComfyUI, control-plane, gpu-fan agent, Server UI with CLI/file explorer/GPU fan curve). Co-authored-by: Cursor <cursoragent@cursor.com>
18 KiB
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/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.mdDOCKER-UI-DEPLOYMENT.mdADR-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) |
| 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)
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
- Dwa panele — porty 8090 i 8091, osobne API key, rozproszony UX.
- Coupling UI+daemon w gpu-fan —
app.pyuruchamia wątek NVML i uvicorn w jednym procesie; static HTML woła/api/*na tym samym origin. - Server-ui nie jest w Dockerze — mimo nazwy „Docker UI” to FastAPI na hoście (
/opt/server-ui,server-ui.service). - Brak jednego entry pointu — NPMPlus domyślnie nie proxyuje 8090/8091; użytkownik musi znać wiele URL.
- „Zarządzanie rootem” — server-ui nie może bezpośrednio pisać do NVML; potrzebuje host agent API.
Wymuszenie root w gpu-fan
# 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.internalna 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 Permissionsna GeForce, ryzyko manual fan po crash - Verdict: Odrzuć — patrz
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)
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:
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:
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.<domena>→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)
systemctlstatus 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(nie0.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_KEYco server-ui lub osobnyAGENT_KEYw env server-ui (nie w repo)
Przykład proxy (szkic 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)
# 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 composew kontenerze wymaga CLI w obrazie lub wywołania Docker API bezpośrednio (obecniecompose_runner.pyużywa subprocess — działa z mounted sock +dockerCLI w image).- Ścieżki
compose_dirwstacks.yamlmuszą istnieć wREPO_ROOTwewną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ć)
- NVML fan control w kontenerze Docker — odrzucone w gpu-fan research.
- Jeden monolityczny proces root z UI na 0.0.0.0 — obecny gpu-fan; do refaktoru.
- Server-ui bez docker.sock a z pretensją do zarządzania kontenerami — wymaga sock lub Docker API.
- Osadzanie ComfyUI w server-ui zamiast linków — duży scope, CORS, osobne sesje.
- Budowa „Proxmox w Dockerze” — over-engineering dla single-node AI homelab.
- 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)
- Raport SERVER-UI-ARCHITECTURE-RESEARCH.md
- ADR-001-host-agent-control-plane.md
- 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.