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>
This commit is contained in:
tomasz-syn-grzegorza
2026-07-05 12:02:04 +00:00
commit 359afb3a59
153 changed files with 18169 additions and 0 deletions
@@ -0,0 +1,451 @@
# 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 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
- **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.<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):**
```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.