Files
ubuntu-bare-metal/coding-agent/SERVER-UI-ARCHITECTURE-RESEARCH.md
T
tomasz-syn-grzegorza 359afb3a59 Initial import: bare-metal stacks, Server UI, GPU fan, tutorials.
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>
2026-07-05 12:02:04 +00:00

18 KiB
Raw Blame History

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:


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

  1. Dwa panele — porty 8090 i 8091, osobne API key, rozproszony UX.
  2. Coupling UI+daemon w gpu-fanapp.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

# stacks/gpu-fan/app.py (linie 161164)
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 (AE)

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 12 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

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.pyhttpx 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)
  • 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):

# 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 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)

  • 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.