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
+19
View File
@@ -0,0 +1,19 @@
# Secrets
.env
**/.env
# Python
.venv/
**/.venv/
__pycache__/
*.pyc
*.pyo
# Local clones / runtime
upstream/
*.pem
# OS / editor
.DS_Store
*.swp
.cursor/
+152
View File
@@ -0,0 +1,152 @@
# ubuntu-bare-metal
Repozytorium konfiguracji czystego Ubuntu na fizycznym serwerze (bare metal) pod hostowanie **vLLM**, **LocalAI**, **ComfyUI** i **Docker**.
Tutorial jest prowadzony krok po kroku — każdy rozdział to samodzielny etap z komendami, weryfikacją i sekcją troubleshooting.
## Sprzęt docelowy
| Parametr | Wartość |
|----------|---------|
| Urządzenie | GMKtec K11 |
| CPU | AMD Phoenix (Ryzen) |
| GPU (LLM / ComfyUI) | NVIDIA GeForce RTX 3090 Ti (24 GB VRAM) |
| iGPU | AMD HawkPoint1 (nieużywane do workloadów AI) |
| RAM | 29 GiB |
| Dysk systemowy | 128 GB NVMe (`/`) — tylko Ubuntu |
| Dysk danych | 1 TB (`/data`) — Docker, modele, aplikacje |
| System | Ubuntu 26.04 LTS (`resolute`) — instalacja minimized |
## Jak korzystać z tutoriala
1. Czytaj rozdziały w kolejności numeracji (`00`, `01`, `02`, …).
2. Wykonuj komendy na serwerze w podanej kolejności.
3. Po każdym rozdziale przejdź sekcję **Weryfikacja** — nie przechodź dalej, dopóki wszystkie punkty nie są zielone.
4. Zgłaszaj poprawki w rozmowie z Cursorem — tutorial będzie aktualizowany.
Instrukcje: **po polsku**. Komendy i nazwy pakietów: **po angielsku** (jak w systemie).
## Roadmapa rozdziałów
| # | Rozdział | Status |
|---|----------|--------|
| 00 | [Wymagania i konwencje](manual-tutorial/00-prerequisites.md) | Gotowy |
| 01 | [Aktualizacja systemu i Docker CE](manual-tutorial/01-system-update-and-docker.md) | Ukończony |
| 02 | [Sterowniki NVIDIA](manual-tutorial/02-nvidia-driver.md) | Ukończony |
| 03 | [NVIDIA Container Toolkit](manual-tutorial/03-nvidia-container-toolkit.md) | Ukończony |
| 03b | [Narzędzia bazowe — Ubuntu minimized](manual-tutorial/03b-system-tools.md) | **Wykonaj przed 04** |
| 04 | [Dysk 1 TB + vLLM stack](manual-tutorial/04-vllm-stack.md) | Ukończony / w toku |
| 05 | [LocalAI stack (UI + GGUF)](manual-tutorial/05-localai-stack.md) | Ukończony / w toku |
| 06 | [Sterowanie wentylatorami GPU](manual-tutorial/06-gpu-fan-control.md) | Gotowy |
| 07 | [ComfyUI stack (Docker)](manual-tutorial/07-comfyui-stack.md) | Gotowy (stack w repo) |
| 08 | [Server UI — panel sterowania](manual-tutorial/08-server-ui-install.md) | Gotowy |
| 09 | Firewall i hardening | Planowany |
```mermaid
flowchart LR
step01["01: apt + Docker CE"]
step02["02: NVIDIA driver"]
step03["03: NVIDIA Container Toolkit"]
step03b["03b: system tools"]
step04["04: disk + vLLM"]
step05["05: LocalAI stack"]
step06["06: ComfyUI stack"]
step07["07: firewall i hardening"]
step01 --> step02 --> step03 --> step03b --> step04
step04 --> step05
step03 --> step06
step05 --> step06
step06 --> step07
```
Kolejność jest krytyczna: **Docker → sterownik GPU → nvidia-container-toolkit → workloady GPU w kontenerach**.
## Architektura dysków
| Dysk | Mount | Zawartość |
|------|-------|-----------|
| 128 GB NVMe | `/` | Ubuntu, `/etc`, `/home`, repo konfiguracyjne |
| 1 TB | `/data` | `/data/docker` (Docker), `/data/apps/*` (modele, LocalAI, ComfyUI) |
Szczegóły konfiguracji dysku 1 TB: rozdział [04 — vLLM stack](manual-tutorial/04-vllm-stack.md) (część A).
## Uwagi
- Do workloadów AI używamy wyłącznie **NVIDIA RTX 3090 Ti** (`CUDA_VISIBLE_DEVICES=0`), nie iGPU AMD.
- vLLM: brak domyślnego modelu — katalog w [`stacks/vllm/models.catalog.yaml`](stacks/vllm/models.catalog.yaml). Szczegóły: [`stacks/vllm/README.md`](stacks/vllm/README.md).
- LocalAI: UI + API na porcie **8070**, start bez modelu — [`stacks/localai/README.md`](stacks/localai/README.md).
- GPU Fan Control: web UI na porcie **8090** (SSH tunnel) — [`stacks/gpu-fan/README.md`](stacks/gpu-fan/README.md).
- Server UI: własny panel stacków na porcie **8091** — [`stacks/server-ui/README.md`](stacks/server-ui/README.md).
- Architektura panelu (gpu-fan + Docker UI): [`coding-agent/SERVER-UI-ARCHITECTURE-RESEARCH.md`](coding-agent/SERVER-UI-ARCHITECTURE-RESEARCH.md)
## Struktura repozytorium
```
ubuntu-bare-metal/
├── README.md
├── scripts/
│ └── setup-data-disk.sh # migracja dysku 1 TB (rozdział 04A)
├── stacks/
│ ├── vllm/
│ │ ├── README.md
│ │ ├── models.catalog.yaml # GGUF + vLLM AWQ, bez auto-download
│ │ ├── docker-compose.yml
│ │ ├── .env.example
│ │ ├── profiles/
│ │ │ ├── _template.env
│ │ │ └── qwen3.6-27b-awq-128k.env
│ │ └── scripts/
│ │ ├── list-models.sh
│ │ ├── download-model.sh
│ │ ├── switch-model.sh
│ │ └── start.sh
│ ├── llamacpp/
│ │ └── README.md # placeholder pod GGUF (port 8001)
│ ├── localai/
│ │ ├── README.md
│ │ ├── docker-compose.yml
│ │ ├── .env.example
│ │ └── scripts/
│ │ ├── clone-upstream.sh
│ │ ├── pull.sh
│ │ └── start.sh
│ ├── comfyui/
│ │ ├── README.md
│ │ ├── docker-compose.yml
│ │ ├── .env.example
│ │ └── scripts/
│ │ ├── ensure-dirs.sh
│ │ ├── pull.sh
│ │ └── start.sh
│ ├── dockge/ # DEPRECATED → server-ui
│ │ └── README.md
│ ├── server-ui/
│ │ ├── README.md
│ │ ├── app.py
│ │ ├── stacks.yaml
│ │ ├── server-ui.service
│ │ ├── static/index.html
│ │ └── scripts/
│ │ ├── install.sh
│ │ └── start.sh
│ └── gpu-fan/
│ ├── README.md
│ ├── app.py
│ ├── fan_controller.py
│ ├── curve.default.json
│ └── scripts/
│ ├── install.sh
│ └── start.sh
└── manual-tutorial/
├── 00-prerequisites.md
├── 01-system-update-and-docker.md
├── 02-nvidia-driver.md
├── 03-nvidia-container-toolkit.md
├── 03b-system-tools.md
├── 04-vllm-stack.md
├── 05-localai-stack.md
├── 06-gpu-fan-control.md
├── 07-comfyui-stack.md
└── ... (kolejne rozdziały)
```
@@ -0,0 +1,102 @@
# ADR-001: Host Agent + Control Plane UI
**Status:** Zaakceptowany (research 2026-07-04)
**Kontekst:** Uniwersalny panel serwera (Server UI) + gpu-fan na gmktec-k11
**Pełna analiza:** [`SERVER-UI-ARCHITECTURE-RESEARCH.md`](SERVER-UI-ARCHITECTURE-RESEARCH.md)
---
## Kontekst i problem
Na hoście działają:
- **gpu-fan** (systemd, root, NVML) — port 8090, własny Web UI
- **server-ui** (systemd) — port 8091, zarządzanie Docker compose
- **Workloady AI** (Docker) — ComfyUI, LocalAI, vLLM
Użytkownik chce:
1. Jeden panel zamiast wielu portów (8090, 8091, …)
2. UI serwerowe w Dockerze
3. Operacje wymagające root (wentylatory GPU) nadal na hoście
Pytanie: jak to pogodzić bez pakowania NVML do kontenera?
---
## Decyzja
Przyjmujemy architekturę **dwuwarstwową**:
1. **Host Agents** — procesy systemd jako root, API tylko na `127.0.0.1`, bez publicznego UI:
- `gpu-fan-daemon` (:18090) — NVML fan loop + REST API
- (przyszłość) `host-agent` — agregacja systemd, disk health
2. **Control Plane UI** — Server UI (docelowo kontener Docker):
- Jeden dashboard, jeden `API_KEY` dla użytkownika
- Mount `/var/run/docker.sock` — orchestracja stacków
- HTTP proxy `/api/gpu-fan/*` → host agent
**Strategia implementacji:** C z planu research (split gpu-fan → integracja UI → dockerize server-ui).
---
## Uzasadnienie
| Alternatywa | Dlaczego odrzucona |
|-------------|-------------------|
| gpu-fan w Dockerze | NVML write na GeForce wymaga root na hoście; kruche, nieutrzymywane |
| Status quo (dwa panele) | Rozproszony UX, dwa klucze |
| Tylko Cockpit | Brak whitelist AI stacks, polityki GPU, custom NVML |
| Monolit root z UI na 0.0.0.0 | Obecny gpu-fan — do refaktoru ze względów bezpieczeństwa |
Wzorzec zgodny z **Portainer** (UI + docker.sock) i **Proxmox** (privileged daemons na hoście).
---
## Konsekwencje
### Pozytywne
- Jeden port publiczny dla panelu (:8091, potem opcjonalnie NPMPlus :443)
- Daemon fan niezależny od UI — bezpieczeństwo termiczne przy padzie panelu
- Możliwość rozszerzenia o `host-agent` bez kolejnych portów LAN
### Negatywne / koszt
- Refactor gpu-fan (rozdzielenie UI i API)
- Dockerize server-ui (Dockerfile, mounty, testy `host.docker.internal`)
- Dwa miejsca deploy: systemd (agenty) + compose (UI)
### Neutralne
- ComfyUI/LocalAI UI pozostają osobnymi aplikacjami (linki z dashboardu)
- npmplus bez zmian (`network_mode: host`)
---
## Szczegóły techniczne (skrót)
```
Browser → server-ui:8091 (Docker)
├─ docker.sock → compose stacks
└─ proxy → 127.0.0.1:18090 (gpu-fan-daemon, systemd root)
```
Env agenta:
- `GPU_FAN_API_HOST=127.0.0.1`
- `GPU_FAN_API_PORT=18090`
Env server-ui:
- `GPU_FAN_AGENT_URL=http://host.docker.internal:18090` (w Dockerze)
---
## Kiedy przejrzeć tę decyzję
- Multi-node cluster (więcej niż jeden host) — wtedy centralny panel + agenty na każdym nodzie
- Migracja na Cockpit/Kubernetes — osobna decyzja ADR
- NVIDIA udostępni oficjalny fan API w kontenerach bez root — mało prawdopodobne na GeForce
---
## Powiązane dokumenty
- [`SERVER-UI-ARCHITECTURE-RESEARCH.md`](SERVER-UI-ARCHITECTURE-RESEARCH.md)
- [`../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)
+94
View File
@@ -0,0 +1,94 @@
# Backlog
Priorytety dla kolejnego agenta. Aktualizuj po ukończeniu zadań.
## P0 — naprawy / dokończenie bieżącego
- [ ] **Przekazać `LOCALAI_API_KEY` do kontenera**
Dodać do `stacks/localai/docker-compose.yml` w sekcji `environment:`:
```yaml
- LOCALAI_API_KEY=${LOCALAI_API_KEY:-}
```
Dodać komentarz / placeholder do `stacks/localai/.env.example` (bez prawdziwego klucza).
Restart: `docker compose --profile localai restart localai`
- [ ] **Zweryfikować API auth po restarcie**
```bash
curl -s http://localhost:8070/v1/models \
-H "Authorization: Bearer <klucz z .env>"
```
Bez klucza powinno być 401.
- [ ] **Zsynchronizować port 8070**
Albo zaktualizować `05-localai-stack.md`, `stacks/localai/README.md`, root README (port 8070), albo zapytać użytkownika o powrót do 8080.
- [ ] **Dokończyć test modelu LocalAI**
Po pobraniu modelu: chat w UI, `curl /v1/chat/completions`, `nvidia-smi` (VRAM).
## P1 — produkcja / dostęp zewnętrzny
- [x] ~~SwarmUI~~ — **usunięte 2026-07-04** ([`SWARMUI-REMOVAL.md`](SWARMUI-REMOVAL.md))
- [ ] Reverse proxy LocalAI — **częściowo** (NPMPlus działa dla `llm.rtx1.mobile.agency-ai.dev`)
- [ ] `LOCALAI_BASE_URL=https://llm.rtx1.mobile.agency-ai.dev` w dokumentacji klienta
- [ ] Bind LocalAI tylko na localhost po proxy:
```yaml
ports:
- "127.0.0.1:${LOCALAI_PORT:-8080}:8080"
```
- [ ] Rozdział **08 — firewall i hardening** (ufw, ograniczenie portów 8070/8090/**8091**)
- [ ] Opcjonalnie: `LOCALAI_AUTH=true` + rejestracja invite (multi-user) zamiast samego legacy key
## P2 — integracja modeli i GPU
- [x] **BGE-Reranker-v2-m3** — GGUF + YAML + `download-reranker.sh` (2026-07-01). Raport: [`RERANKER-DEPLOYMENT.md`](RERANKER-DEPLOYMENT.md)
- [ ] Połączyć `stacks/vllm/models.catalog.yaml` z LocalAI
GGUF z `/data/apps/gguf/` → symlink lub kopia do `/data/apps/localai/models/` + YAML
- [ ] Skrypt pobierania modelu pod LocalAI (obok vLLM `download-model.sh`)
- [ ] vLLM opcjonalnie: `./scripts/download-model.sh qwen3.6-27b-awq-vllm` + `switch-model.sh`
Tylko gdy użytkownik chce AWQ zamiast GGUF w LocalAI
- [x] Dokumentacja: jeden aktywny duży model na GPU — LocalAI ↔ ComfyUI, procedura w Server UI — [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md), [`stacks/comfyui/README.md`](../stacks/comfyui/README.md)
- [x] ~~Dockge~~ — **usunięty 2026-07-04** ([`DOCKGE-REMOVAL.md`](DOCKGE-REMOVAL.md))
- [x] **Server UI** — `stacks/server-ui/`, port **8091** (2026-07-04)
- [ ] **Produkcja server-ui** — `sudo ./scripts/install.sh` na hoście (systemd)
- [ ] **Architektura Server UI + gpu-fan** — research: [`SERVER-UI-ARCHITECTURE-RESEARCH.md`](SERVER-UI-ARCHITECTURE-RESEARCH.md), ADR: [`ADR-001-host-agent-control-plane.md`](ADR-001-host-agent-control-plane.md)
- Faza 1: gpu-fan API-only `127.0.0.1:18090`
- Faza 2: UI gpu-fan w server-ui (proxy)
- Faza 3: server-ui w Dockerze (`docker.sock` + mount repo)
## P3 — roadmapa tutoriala
- [x] Rozdział **07 — ComfyUI stack** (Docker, `/data/apps/comfyui/`) — stack + tutorial 2026-07-04. Research SM: [`STABILITYMATRIX-RESEARCH.md`](STABILITYMATRIX-RESEARCH.md)
- [ ] **Uruchomić ComfyUI na hoście** — `cd stacks/comfyui && ./scripts/start.sh` (użytkownik)
- [ ] Pełny Docker llama.cpp — **niski priorytet** jeśli LocalAI wystarczy dla GGUF
- [ ] Open WebUI dla vLLM — opcjonalnie, nie planowane na teraz
## Czego nie robić bez prośby użytkownika
- `git commit` / `git push`
- Pobieranie modeli LLM przy `docker compose pull`
- Build LocalAI lub vLLM ze źródeł GitHub
- Edycja plików w `.cursor/plans/`
- Zapisywanie sekretów w `coding-agent/` lub w commitach
## Ukończone w tej sesji (referencja)
- [x] Tutorial 0003b, 04 (dysk + vLLM), 05 (LocalAI)
- [x] `stacks/vllm/` — katalog modeli, skrypty, elastyczny compose
- [x] `stacks/localai/` — stack GPU cuda-13, skrypty, README
- [x] `stacks/llamacpp/README.md` — placeholder
- [x] `stacks/server-ui/` — własny Docker UI, port 8091 (2026-07-04)
- [x] `scripts/setup-data-disk.sh` — katalogi aplikacji na `/data`
- [x] Root README — roadmapa 05/06/07
- [x] `stacks/comfyui/` — ComfyUI Docker, port 8188 (2026-07-04); research SM → [`STABILITYMATRIX-RESEARCH.md`](STABILITYMATRIX-RESEARCH.md)
- [x] `manual-tutorial/07-comfyui-stack.md` — tutorial ComfyUI
+119
View File
@@ -0,0 +1,119 @@
# Wdrożenie ComfyUI stack (Docker)
Data: 2026-07-04
Host: `gmktec-k11` — RTX 3090 Ti 24 GB, `/data` zamontowany
## Kontekst
Po researchu [Stability Matrix](STABILITYMATRIX-RESEARCH.md) — **nie instalujemy SM** (wymaga GUI). Zamiast tego ComfyUI w Dockerze, wzorowany na `stacks/localai/`.
SwarmUI usunięte 2026-07-04 — [`SWARMUI-REMOVAL.md`](SWARMUI-REMOVAL.md).
## Podsumowanie stacku
| Element | Wartość |
|---------|---------|
| Stack | [`stacks/comfyui/`](../stacks/comfyui/) |
| Kontener | `comfyui` |
| Obraz | `yanwk/comfyui-boot:cu126-slim` |
| Port | **8188** |
| Profil compose | `comfyui` |
| Dane | `/data/apps/comfyui/` |
## Architektura
```mermaid
flowchart TB
subgraph host [gmktec-k11 headless]
localai[localai :8070]
comfyui[comfyui :8188]
dockge[server-ui :8091]
end
data["/data/apps/comfyui/"]
comfyui --> data
note["GPU: localai LUB comfyui — nie oba z dużym modelem"]
```
## Pliki w repo
| Plik | Rola |
|------|------|
| `stacks/comfyui/docker-compose.yml` | GPU, port 8188, bind mounty |
| `stacks/comfyui/.env.example` | `DATA_ROOT`, `COMFYUI_PORT`, obraz |
| `stacks/comfyui/scripts/start.sh` | ensure-dirs, pull, up, ostrzeżenie o LocalAI |
| `stacks/comfyui/scripts/ensure-dirs.sh` | katalogi na `/data` |
| `stacks/comfyui/scripts/pull.sh` | tylko `docker compose pull` |
| `stacks/comfyui/README.md` | dokumentacja stacku |
## Katalogi na hoście
```
/data/apps/comfyui/
├── storage/ # kopia ComfyUI (pierwszy start obrazu yanwk)
├── models/
├── cache/hf-hub/
├── cache/torch-hub/
├── input/
├── output/
├── custom_nodes/
└── workflows/
```
Istniejące katalogi z `setup-data-disk.sh` (models, input, output, custom_nodes) są rozszerzone przez `ensure-dirs.sh`.
## Procedura wdrożenia
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/comfyui
cp .env.example .env
# opcjonalnie: zatrzymaj LocalAI przed dużym modelem SD
cd ../localai && docker compose --profile localai stop localai
cd ../comfyui
./scripts/start.sh
```
Weryfikacja:
```bash
docker compose --profile comfyui ps
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8188/
nvidia-smi
```
Oczekiwany HTTP: **200** (po `start_period` healthcheck, ~3 min pierwszy start).
## Polityka GPU
| Scenariusz | Akcja |
|------------|-------|
| Chat LLM (LocalAI) | `comfyui` stopped lub bez modelu w VRAM |
| Generowanie obrazów (ComfyUI) | `localai` stopped |
| Portainer | Stop/Start kontenerów według potrzeb |
## Modele
- Brak auto-download w `compose pull`
- Użytkownik: ComfyUI-Manager w UI lub ręcznie do `/data/apps/comfyui/models/`
- Szacunki VRAM: SDXL ~812 GB; Flux ~1220 GB (mieści się na 24 GB przy zatrzymanym LocalAI)
## Dostęp sieciowy
| Sieć | URL |
|------|-----|
| localhost | `http://127.0.0.1:8188` |
| LAN (po statycznym IP) | `http://192.168.100.90:8188` (enp3s0) |
| Reverse proxy | opcjonalnie później przez NPMPlus (jak LocalAI) |
## Szacowany rozmiar
- Obraz Docker: ~515 GB (zależnie od wariantu yanwk)
- Modele SD/Flux: dziesiątki GB — użytkownik pobiera sam
## Powiązane dokumenty
- [`COMFYUI-HANDOFF.md`](COMFYUI-HANDOFF.md)
- [`STABILITYMATRIX-RESEARCH.md`](STABILITYMATRIX-RESEARCH.md)
- [`manual-tutorial/07-comfyui-stack.md`](../manual-tutorial/07-comfyui-stack.md)
- [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md) — polityka GPU w Portainer
+82
View File
@@ -0,0 +1,82 @@
# Handoff: ComfyUI stack
Data: 2026-07-04
Dla kolejnego agenta / sesji Cursor
## Co zrobiono
1. Research Stability Matrix → decyzja **nie instalować** ([`STABILITYMATRIX-RESEARCH.md`](STABILITYMATRIX-RESEARCH.md))
2. Utworzono pełny stack [`stacks/comfyui/`](../stacks/comfyui/) — wzorzec `localai`
3. Dokumentacja wdrożenia i tutorial rozdział 07
4. Zaktualizowano BACKLOG, Portainer README, politykę GPU LocalAI ↔ ComfyUI
## Co użytkownik musi zrobić na hoście
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/comfyui
cp .env.example .env
./scripts/start.sh
```
Pierwszy start: kopia ComfyUI do `/data/apps/comfyui/storage/` — kilka minut, duży pull obrazu.
Przed generowaniem obrazów z dużym modelem:
```bash
cd ../localai && docker compose --profile localai stop localai
```
## Weryfikacja po wdrożeniu
```bash
docker ps --filter name=comfyui
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8188/
nvidia-smi
```
UI: `http://192.168.100.90:8188` (LAN, enp3s0) lub localhost.
## Otwarte / następne kroki
| Priorytet | Zadanie |
|-----------|---------|
| P1 | Uruchomić stack na hoście (`./scripts/start.sh`) — **nie wykonane w repo-only** |
| P2 | Pobrać pierwszy checkpoint SD/SDXL do `models/` |
| P2 | NPMPlus reverse proxy dla ComfyUI (opcjonalnie, jak LocalAI) |
| P3 | Bind `127.0.0.1:8188` po proxy (bezpieczeństwo) |
## Polityka GPU (przypomnienie)
RTX 3090 Ti 24 GB — **jeden** duży workload GPU:
- LocalAI (LLM) **lub** ComfyUI (SD/Flux) z modelem w VRAM
- W Portainer: Stop/Start `localai` / `comfyui`
## Pliki kluczowe
```
stacks/comfyui/
├── docker-compose.yml # profile comfyui, port 8188, gpus: all
├── .env.example
└── scripts/start.sh # ostrzeżenie gdy localai running
coding-agent/
├── STABILITYMATRIX-RESEARCH.md
├── COMFYUI-DEPLOYMENT.md
└── COMFYUI-HANDOFF.md # ten plik
manual-tutorial/07-comfyui-stack.md
```
## Czego nie robić bez prośby
- `git commit` / `git push`
- Pobieranie modeli SD przy `docker compose pull`
- Instalacja Stability Matrix AppImage
- Edycja `.cursor/plans/`
## Powiązane
- [`BACKLOG.md`](BACKLOG.md)
- [`STATE.md`](STATE.md) — zaktualizuj po uruchomieniu na hoście
- [`SWARMUI-REMOVAL.md`](SWARMUI-REMOVAL.md)
+149
View File
@@ -0,0 +1,149 @@
# Control plane — jeden plik `.env`
**Data:** 2026-07-05
**Status:** wdrożone
gpu-fan (agent NVML) i Server UI (panel stacków) używają **jednego** pliku credentials.
## Kanoniczne ścieżki
| Środowisko | Plik |
|------------|------|
| Produkcja (systemd) | `/opt/control-plane/.env` |
| Dev w repo | `stacks/control-plane/.env` |
| Szablon | `stacks/control-plane/.env.example` |
Oba serwisy systemd:
```ini
EnvironmentFile=-/opt/control-plane/.env
```
- `server-ui.service`
- `gpu-fan.service`
## Zmienne
| Zmienna | Opis |
|---------|------|
| `API_KEY` | **Wspólny** klucz: auth panelu Server UI (`X-API-Key`) + auth agenta gpu-fan (proxy `/api/gpu-fan/*`) |
| `SERVER_UI_HOST`, `SERVER_UI_PORT` | Bind Server UI (domyślnie `0.0.0.0:8091`) |
| `REPO_ROOT` | Ścieżka do `ubuntu-bare-metal` (wymagane dla Docker Server UI) |
| `DOCKER_GID` | GID grupy docker (Docker install) |
| `GPU_FAN_AGENT_URL` | URL agenta z perspektywy Server UI (`http://127.0.0.1:18090` native, `http://host.docker.internal:18090` Docker) |
| `GPU_FAN_API_HOST`, `GPU_FAN_API_PORT` | Bind agenta gpu-fan (domyślnie `127.0.0.1:18090`) |
| `CURVE_PATH`, `POLL_INTERVAL`, `GPU_INDEX` | Konfiguracja NVML / krzywej wentylatorów |
**Usunięte:** `GPU_FAN_AGENT_KEY` — duplikat `API_KEY`.
## Ładowanie w Pythonie
[`stacks/control-plane/env_loader.py`](../stacks/control-plane/env_loader.py) — `load_control_plane_env(stack_dir)`:
**Produkcja** (`stack_dir` pod `/opt/`): tylko `/opt/control-plane/.env` + `os.environ` (systemd).
**Dev** (repo `stacks/*`): tylko `stacks/control-plane/.env` + `os.environ`.
**Nie ładowane:** `stacks/server-ui/.env`, `/opt/server-ui/.env` (legacy — `API_KEY` usuwany przez `setup-control-plane-env.sh`).
Kolejność priorytetu: pliki env → **`os.environ` wygrywa** (systemd `EnvironmentFile`).
`api_key_source(stack_dir, values)` — log przy starcie Server UI (bez ujawniania sekretu).
Produkcja: `env_loader.py` kopiowany do `/opt/control-plane/env_loader.py` przez `setup-control-plane-env.sh`.
## Instalacja / migracja
```bash
sudo bash stacks/server-ui/scripts/setup-control-plane-env.sh
sudo systemctl daemon-reload
sudo systemctl restart gpu-fan server-ui
```
Skrypt:
- tworzy `/opt/control-plane/.env` z example jeśli brak
- **migruje** z `/opt/server-ui/.env` i `/opt/gpu-fan/.env` (backup `.env.bak.<timestamp>`)
- generuje `API_KEY` jeśli brak lub `change-me`
- **sync** `stacks/control-plane/.env` z produkcją (ten sam `API_KEY`)
- **usuwa** `API_KEY` / `GPU_FAN_AGENT_KEY` z legacy `stacks/server-ui/.env`
- wypisuje gotowy URL z kluczem (`print-api-key-instructions.sh`)
Instrukcja dla użytkownika po instalacji:
```bash
bash stacks/server-ui/scripts/show-api-key.sh
```
Pełna instalacja:
```bash
sudo bash stacks/server-ui/scripts/install-control-plane.sh
```
## Weryfikacja
```bash
bash stacks/server-ui/scripts/show-api-key.sh
API_KEY=$(sudo grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)
# Test klucza w panelu
curl -s -X POST http://127.0.0.1:8091/api/auth/verify \
-H "Content-Type: application/json" \
-d "{\"api_key\":\"${API_KEY}\"}"
# Agent bezpośrednio
curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: ${API_KEY}"
# Proxy przez Server UI
curl -s http://127.0.0.1:8091/api/gpu-fan/health -H "X-API-Key: ${API_KEY}"
```
Oczekiwane: JSON z danymi / `"ok": true`.
## Docker Server UI
[`stacks/server-ui/docker-compose.yml`](../stacks/server-ui/docker-compose.yml):
```yaml
env_file:
- ../control-plane/.env
```
## Auth flow
```mermaid
sequenceDiagram
participant Browser
participant ServerUI
participant GpuFanAgent
Browser->>ServerUI: X-API-Key: API_KEY
ServerUI->>GpuFanAgent: X-API-Key: API_KEY (ten sam)
GpuFanAgent-->>ServerUI: status JSON
ServerUI-->>Browser: proxy response
```
## Poza scope
Sekrety workloadów Docker (`LOCALAI_API_KEY`, `HF_TOKEN`, `INITIAL_ADMIN_PASSWORD`, …) pozostają w `stacks/<nazwa>/.env` — edycja portów przez Server UI bez zmian.
## Pliki zmienione (implementacja)
- `stacks/control-plane/``.env.example`, `env_loader.py`
- `stacks/server-ui/scripts/setup-control-plane-env.sh`
- `server-ui.service`, `gpu-fan.service`
- `server-ui/app.py`, `gpu_fan_proxy.py`, `gpu-fan/fan_daemon.py`, `gpu-fan/app.py`
- Instalatory: `install-control-plane.sh`, `install.sh`, `install-docker.sh`, `gpu-fan/install.sh`
- Skrypty dev: `start.sh`, `deploy-gpu-fan-fix.sh`, `restart-stack.sh`, `enable-lan.sh`, `status.sh`
## Troubleshooting — Invalid API key
1. Klucz tylko z `/opt/control-plane/.env` (`sudo grep ^API_KEY= …`)
2. UI: **Zapisz****Sprawdź klucz**
3. Sync: `sudo bash stacks/server-ui/scripts/setup-control-plane-env.sh`
4. Tutorial: [`manual-tutorial/04a-api-key.md`](../manual-tutorial/04a-api-key.md)
## Legacy
Stare pliki `/opt/server-ui/.env` i `/opt/gpu-fan/.env` nie są już czytane przez systemd. Po migracji można je usunąć (backup w `.env.bak.*`).
+91
View File
@@ -0,0 +1,91 @@
# Konwencje projektu
Zasady spójne dla wszystkich stacków w `ubuntu-bare-metal`.
## Stack Docker
Każdy stack w `stacks/<nazwa>/`:
```
stacks/<nazwa>/
├── README.md
├── docker-compose.yml
├── .env.example # szablon bez sekretów
├── .env # lokalny, w .gitignore
├── .gitignore
└── scripts/
├── start.sh # walidacja + pull + up
└── pull.sh # opcjonalnie — tylko obraz
```
- **Profile compose:** `profiles: [<nazwa>]` — start jawny: `docker compose --profile <nazwa> up -d`
- **GPU:** `gpus: all` + `CUDA_VISIBLE_DEVICES=0` (tylko RTX 3090 Ti)
- **Dane:** bind mounty pod `${DATA_ROOT}/apps/<nazwa>/`, nie named volumes (backup na `/data`)
- **Restart:** `restart: unless-stopped` na produkcji
## Zmienne środowiskowe
| Zmienna | Typowy zakres |
|---------|----------------|
| `DATA_ROOT` | `/data` |
| `CUDA_VISIBLE_DEVICES` | `0` |
| `HF_TOKEN` | vLLM — tylko gated models |
| `LOCALAI_API_KEY` | LocalAI — legacy API auth |
| `LOCALAI_PORT` | domyślnie 8080 (użytkownik może zmienić, np. 8070) |
| `COMFYUI_PORT` | domyślnie 8188 |
| `VLLM_PORT` | domyślnie 8000 |
W **Server UI** porty edytowalnych stacków są zapisywane w `stacks/<nazwa>/.env`. Metadane (`port_env`, `port_default`, `port_editable`) w `stacks/server-ui/stacks.yaml`.
Sekrety **tylko** w `.env` na serwerze. W `.env.example` — puste lub komentarz.
### Control plane (gpu-fan + Server UI)
Jeden plik credentials — **nie** osobne `/opt/server-ui/.env` i `/opt/gpu-fan/.env`:
| Środowisko | Plik |
|------------|------|
| Produkcja | `/opt/control-plane/.env` |
| Dev | `stacks/control-plane/.env` |
Wspólny `API_KEY` dla panelu i proxy gpu-fan. Szczegóły: [`CONTROL-PLANE-ENV.md`](CONTROL-PLANE-ENV.md).
## Tutorial (`manual-tutorial/`)
- Numeracja: `00`, `01`, … `03b`, `04`, `05`, …
- Język: polski; komendy i nazwy pakietów po angielsku
- Każdy rozdział: cel, wymagania, komendy, weryfikacja, troubleshooting
- Użytkownik potwierdza ukończenie („krok X gotowy”) przed kolejnym rozdziałem
## Modele LLM
- **Brak auto-download** przy `docker compose pull`
- Katalog centralny vLLM: `stacks/vllm/models.catalog.yaml`
- GGUF → LocalAI lub llama.cpp; AWQ/HF → vLLM
- Kilka modeli na dysku, **jeden aktywny w VRAM**
## Git i bezpieczeństwo
- Nie commituj: `.env`, `upstream/`, kluczy API, tokenów HF
- `coding-agent/` — bez wartości sekretów
- Commity tylko na wyraźną prośbę użytkownika
- Nie `git push --force` na main/master
## Ścieżki bezwzględne na serwerze
Domyślny root repo:
```
/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal
```
W tutorialach używaj tej pełnej ścieżki w przykładach `cd`.
## GMKtec K11 — numeracja NVMe
| Urządzenie | Rozmiar | Rola |
|------------|---------|------|
| `nvme1n1` | ~128 GB | System `/` |
| `nvme0n1` | ~1 TB | Dane `/data` |
Zawsze weryfikuj po `SIZE` i `MOUNTPOINT`, nie po numerze urządzenia.
+90
View File
@@ -0,0 +1,90 @@
# Wdrożenie UI zarządzania Dockerem (Server UI)
Data: 2026-07-05 (aktualizacja: instalator control plane)
Host: `gmktec-k11` (`192.168.100.90` — enp3s0)
## Kontekst
- Portainer usunięty 2026-07-04 — [`PORTAINER-REMOVAL.md`](PORTAINER-REMOVAL.md)
- Dockge usunięty 2026-07-04 — [`DOCKGE-REMOVAL.md`](DOCKGE-REMOVAL.md)
- **Własny panel:** [`stacks/server-ui/`](../stacks/server-ui/) — port **8091**
- **gpu-fan:** host agent :18090 (tylko systemd, bez Docker)
---
## Podsumowanie
| Element | Wartość |
|---------|---------|
| Stack | [`stacks/server-ui/`](../stacks/server-ui/) |
| Runtime native | systemd (`server-ui.service`) → `/opt/server-ui` |
| Runtime Docker | `docker compose --profile server-ui` (opcjonalnie) |
| HTTP | **http://192.168.100.90:8091** |
| Auth mutacji | `X-API-Key``/opt/control-plane/.env` (native) lub `stacks/control-plane/.env` (docker) |
| Stacki | `localai`, `vllm`, `comfyui`, `npmplus` |
**Server UI nie pojawia się w `docker ps` przy instalacji native** — to usługa systemd, nie kontener.
---
## Instalacja (zalecana)
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/server-ui
sudo ./scripts/install-control-plane.sh
```
Menu: gpu-fan native (Y/n) + Server UI native (1) / Docker (2) / skip (3).
```bash
# Bez pytań: gpu-fan + server-ui native
sudo ./scripts/install-control-plane.sh -y
# Tylko Server UI Docker
sudo ./scripts/install-control-plane.sh --gpu-fan=no --server-ui=docker
```
### Tylko Server UI
| Tryb | Komenda |
|------|---------|
| Native | `sudo ./scripts/install.sh` |
| Docker | `sudo ./scripts/install-docker.sh` |
Klucze:
```bash
grep ^API_KEY= /opt/control-plane/.env
```
Tutorial: [`manual-tutorial/08-server-ui-install.md`](../manual-tutorial/08-server-ui-install.md)
---
## Pierwsze użycie
1. Otwórz **http://192.168.100.90:8091/?api_key=KLUCZ**
2. Karty stacków: Start/Stop, logi, edycja portów, linki (8070, 8188, …)
3. Zakładka **GPU Fan** — wymaga działającego `gpu-fan.service`
## Polityka GPU
RTX 3090 Ti 24 GB — jeden duży workload GPU naraz. API zwraca **409** przy konflikcie.
## Weryfikacja
```bash
# Native
systemctl status server-ui gpu-fan
curl -s http://127.0.0.1:8091/api/health
# Docker
docker compose --profile server-ui ps
curl -s http://127.0.0.1:8091/api/stacks
```
## Powiązane
- [`SERVER-UI-INSTALL-OPTIONS.md`](SERVER-UI-INSTALL-OPTIONS.md)
- [`SERVER-UI-PORT-CONFIG.md`](SERVER-UI-PORT-CONFIG.md)
- [`DOCKER-UI-HANDOFF.md`](DOCKER-UI-HANDOFF.md)
- [`stacks/server-ui/README.md`](../stacks/server-ui/README.md)
+55
View File
@@ -0,0 +1,55 @@
# Handoff — Server UI (Docker stack manager)
## Stan: kod gotowy (2026-07-04)
- [x] `stacks/server-ui/` — app, API, dashboard, systemd unit
- [x] Dockge usunięty z Dockera
- [ ] **Produkcja:** `sudo ./scripts/install.sh` (wymaga hasła sudo na hoście)
- [x] Dev mode zweryfikowany: `:8091` HTTP 200, `/api/gpu`, `/api/stacks`
Raport: [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md)
---
## URL
```
http://192.168.100.90:8091
```
API Key po `sudo ./scripts/install.sh`:
```bash
grep ^API_KEY= /opt/control-plane/.env
```
Dev mode (z repo):
```bash
cp stacks/control-plane/.env.example stacks/control-plane/.env
cd stacks/server-ui && ./scripts/start.sh
```
---
## Zarządzanie GPU
Stop/Start stacków `localai` / `comfyui` / `vllm` w UI — jeden aktywny GPU workload naraz.
---
## Czego nie robić
- Nie przywracać Portainera/Dockge bez uzgodnienia
- Nie commitować `.env` z API_KEY
- Nie `git commit` bez prośby użytkownika
---
## Diagnostyka
```bash
systemctl status server-ui
journalctl -u server-ui -f
curl -s http://127.0.0.1:8091/api/stacks
```
+36
View File
@@ -0,0 +1,36 @@
# Usunięcie Dockge
Data: 2026-07-04
Host: `gmktec-k11`
## Powód
Dockge wdrożony tymczasowo po usunięciu Portainera. Zastąpiony **własnym Server UI** ([`stacks/server-ui/`](../stacks/server-ui/)) — skupiony panel pod znane stacki, bez zewnętrznej zależności.
## Co usunięto
| Element | Status |
|---------|--------|
| Kontener `dockge` | usunięty |
| Obraz `louislam/dockge:1` | usunięty |
| Stack Docker | `docker compose --profile dockge down` |
## Co pozostało (opcjonalnie do ręcznego cleanup)
```
/data/apps/dockge/data/ # dane Dockge (nieużywane)
/data/apps/stacks/ # symlinki do repo (nieużywane przez Server UI)
stacks/dockge/ # deprecated w repo
```
## Zamiennik
**Server UI** — port **8091**, systemd na hoście:
- [`stacks/server-ui/README.md`](../stacks/server-ui/README.md)
- [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md)
## Powiązane
- [`PORTAINER-REMOVAL.md`](PORTAINER-REMOVAL.md)
- [`DOCKER-UI-HANDOFF.md`](DOCKER-UI-HANDOFF.md)
+113
View File
@@ -0,0 +1,113 @@
# Przygotowanie repozytorium pod Gitea
Raport z sesji przygotowującej pierwszy commit `ubuntu-bare-metal`.
## Analiza stanu wyjściowego
| Aspekt | Wynik |
|--------|--------|
| Git | Katalog **nie był** repozytorium — wykonano `git init` |
| Gitea remote | **Nie skonfigurowany** — URL do podania przez użytkownika |
| Pliki `.env` | 7 na dysku (sekrety) — **wykluczone** przez root `.gitignore` |
| `.venv` | `stacks/server-ui/.venv`, `stacks/gpu-fan/.venv` (~54 MB każdy) — **wykluczone** |
| Bloker bezpieczeństwa | Prawdziwy `API_KEY` w `manual-tutorial/04a-api-key.md`**zredagowany** na placeholder |
## Zmiany przygotowawcze (ta sesja)
1. **Root [`.gitignore`](../.gitignore)**`.env`, `.venv`, `__pycache__`, `upstream/`, `.cursor/`
2. **Redakcja** [`manual-tutorial/04a-api-key.md`](../manual-tutorial/04a-api-key.md) — przykład `API_KEY` to placeholder `xxxxxxxx…`, nie produkcyjny klucz
3. **`git init`** + branch `main`
4. **Pierwszy commit** — pełny import repo (bez push)
## Zakres pierwszego commita
```
ubuntu-bare-metal/
├── README.md
├── .gitignore
├── scripts/
├── manual-tutorial/ # 13 rozdziałów (0010, 04a)
├── coding-agent/ # handoff, ADR, deployment notes
└── stacks/
├── vllm/
├── localai/
├── comfyui/
├── gpu-fan/
├── server-ui/ # panel :8091, CLI PTY, Pliki, GPU Fan
├── control-plane/ # env_loader, wspólny API_KEY
├── npmplus/
└── llamacpp/
```
### Nie trafia do gita
- `**/.env` (produkcja: `/opt/control-plane/.env` poza repo)
- `**/.venv/`, `**/__pycache__/`
- `stacks/localai/upstream/` (jeśli sklonowany lokalnie)
## Ostatnie zmiany Server UI (GPU Fan chart)
W [`stacks/server-ui/static/index.html`](../stacks/server-ui/static/index.html):
- Siatka i podziałka temperatury (°C) / prędkości (%)
- Linijki kreskowe (zamiast kolorowych pasków gradientowych)
- Odstęp etykiet Y od lewej linijki (`yLabelX`)
- Odstęp etykiet X od dolnej linijki (`xLabelY`)
- Responsywne SVG: `aspect-ratio: 600/360`, `height: auto` (bez rozciągania na mobile)
- Wyśrodkowanie wykresu w karcie: `margin: 0 auto`
Powiązane docs: [`manual-tutorial/06-gpu-fan-control.md`](../manual-tutorial/06-gpu-fan-control.md), [`SERVER-UI-CLI.md`](SERVER-UI-CLI.md), [`SERVER-UI-CLI-BUGFIX.md`](SERVER-UI-CLI-BUGFIX.md).
## Push na Gitea (gdy masz URL)
```bash
cd ~/cursor/ubuntu-bare-metal
# jednorazowo — podstaw URL z Gitea (SSH lub HTTPS)
git remote add origin <URL_GITEA>
# jeśli repo na Gitea ma już README / initial commit:
# git pull origin main --allow-unrelated-histories
# (rozwiąż ewentualne konflikty, potem push)
git push -u origin main
```
Przykładowe URL:
- HTTPS: `https://gitea.example.com/user/ubuntu-bare-metal.git`
- SSH: `git@gitea.example.com:user/ubuntu-bare-metal.git`
## Checklist bezpieczeństwa przed każdym commitem
```bash
cd ~/cursor/ubuntu-bare-metal
# 1. Brak sekretów w staged plikach
git diff --cached | grep -iE 'API_KEY=|HF_TOKEN=|password=|secret=' && echo 'STOP: sekret w diff!' || echo 'OK'
# 2. .env nie jest śledzony
git status --short | grep '\.env' && echo 'STOP: .env w staging!' || echo 'OK'
# 3. Dry-run — co trafi do commita
git add -n .
# 4. Szukaj przypadkowych kluczy w docs
grep -rE '[a-f0-9]{32}' manual-tutorial/ coding-agent/ --include='*.md' | grep -v xxxxxxxx
```
## Zalecenia po redakcji klucza w tutorialu
Klucz produkcyjny był kiedyś w `04a-api-key.md`. Rozważ **rotację** `API_KEY` w `/opt/control-plane/.env` i restart `server-ui` / `gpu-fan`, jeśli repo będzie publiczne lub współdzielone.
## Weryfikacja po clone na innym hoście
```bash
git clone <URL_GITEA> ubuntu-bare-metal
cd ubuntu-bare-metal/stacks/control-plane
cp .env.example .env # uzupełnij API_KEY lokalnie
```
---
*Wygenerowano przy pierwszym imporcie git — lipiec 2026.*
+104
View File
@@ -0,0 +1,104 @@
# Handoff — sesja ubuntu-bare-metal
Dokument dla agenta kontynuującego pracę. Ostatni znany stan po sesji konfiguracji serwera LLM na GMKtec K11.
## Kontekst projektu
| Element | Wartość |
|---------|---------|
| Repo | `/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal` |
| Urządzenie | GMKtec K11 |
| OS | Ubuntu 26.04 LTS minimized (`resolute`) |
| GPU | NVIDIA GeForce RTX 3090 Ti, 24 GB VRAM (`CUDA_VISIBLE_DEVICES=0`) |
| RAM | ~29 GiB |
| Dysk systemowy | `nvme1n1` 128 GB → `/` |
| Dysk danych | `nvme0n1` 1 TB → `/data` |
| Docker data-root | `/data/docker` |
| Użytkownik | `tomasz-syn-grzegorza` (sudo) |
**Workflow:** tutorial krok po kroku w [`manual-tutorial/`](../manual-tutorial/). Użytkownik czyta markdown i wykonuje komendy ręcznie. Agent nie może wpisać hasła `sudo` interaktywnie — skrypty wymagające sudo użytkownik uruchamia w swoim terminalu SSH.
## Ukończone (infrastruktura + repo)
| Rozdział / obszar | Status |
|-------------------|--------|
| 01 — Docker CE ~29.x | Ukończony przez użytkownika |
| 02 — NVIDIA driver 595 open | Ukończony |
| 03 — NVIDIA Container Toolkit 1.19 | Ukończony |
| 03b — narzędzia minimized | Dokumentacja gotowa |
| 04A — dysk 1 TB, fstab, migracja Docker | Ukończony (`scripts/setup-data-disk.sh`) |
| 04B — stack vLLM | Repo gotowe; obraz Docker pobrany; **kontener nie uruchomiony** (brak modelu) |
| Katalog modeli vLLM | `stacks/vllm/models.catalog.yaml` + skrypty list/download/switch |
| Placeholder llama.cpp | `stacks/llamacpp/README.md` |
| 05 — stack LocalAI | Repo gotowe; **kontener uruchomiony przez użytkownika** |
| LocalAI API key | Użytkownik dodał `LOCALAI_API_KEY` do `stacks/localai/.env`**wartość nie dokumentowana tutaj** |
## Stan runtime (faktyczny na serwerze)
| Serwis | Stan |
|--------|------|
| vLLM | Obraz `vllm/vllm-openai:latest` na dysku; kontener **nie działa** (`VLLM_MODEL` puste w `.env`) |
| LocalAI | Kontener `localai` **running**; obraz `localai/localai:v4.4.3-gpu-nvidia-cuda-13` |
| Model LocalAI | Użytkownik **w trakcie pobierania** modelu (prawdopodobnie przez UI / galerię) |
| API key | Ustawiony w `.env`**wymaga** przekazania do kontenera w `docker-compose.yml` + restart (patrz BACKLOG P0) |
## Porty
| Serwis | Port w dokumentacji | Port faktyczny (`.env` użytkownika) |
|--------|---------------------|-------------------------------------|
| vLLM API | 8000 | 8000 (nieaktywny) |
| LocalAI UI + API | 8080 | **8070** |
| llama.cpp (plan) | 8001 | nie wdrożony |
Wewnątrz kontenera LocalAI zawsze nasłuchuje na **8080**; mapowanie hosta ustawia `LOCALAI_PORT` w `.env`.
## Decyzje techniczne (nie zmieniać bez uzgodnienia)
1. **GGUF** z lmstudio-community **nie działa** w standardowym `vllm/vllm-openai` — użycie przez LocalAI (llama.cpp backend) lub przyszły host llama.cpp.
2. **vLLM interim** dla Q4-odpowiednika: AWQ `Qwen/Qwen3.6-27B-Instruct-AWQ`, profil `qwen3.6-27b-awq-128k`, kontekst 128K (`MAX_MODEL_LEN=131072`), KV `fp8`.
3. **LocalAI:** oficjalny obraz Docker (nie build ze źródeł); bez `command: phi-2` (brak auto-pobierania modelu przy starcie).
4. **Jeden duży model w VRAM** naraz na 24 GB — nie uruchamiać vLLM + LocalAI z dużymi modelami równolegle.
5. **Modele docelowe (katalog):** Qwen3.6-27B Q4_K_M GGUF, Gemma 4 12B Q4_0 GGUF (lmstudio-community).
6. **Brak domyślnego modelu** przy instalacji stacków — pobieranie on-demand.
## Co zrobiono w repo (implementacja agenta)
### vLLM (`stacks/vllm/`)
- `models.catalog.yaml` — GGUF (llamacpp) + AWQ (vllm)
- Elastyczny `docker-compose.yml` + `vllm-entrypoint.sh` (`QUANTIZATION`, `VLLM_EXTRA_ARGS`)
- Skrypty: `list-models.sh`, `download-model.sh`, `switch-model.sh`, `start.sh`
- Profile: `qwen3.6-27b-awq-128k.env`, `_template.env`
- Tutorial część B w `manual-tutorial/04-vllm-stack.md`
### LocalAI (`stacks/localai/`)
- `docker-compose.yml`, `.env.example`, skrypty `pull.sh`, `start.sh`, `clone-upstream.sh`
- Bind mounty na `/data/apps/localai/{models,backends,configuration,images,data}`
- Tutorial `manual-tutorial/05-localai-stack.md`
- Roadmapa root README: 05 LocalAI, 06 ComfyUI, 07 firewall
### Inne
- `scripts/setup-data-disk.sh` — rozszerzony o katalogi `gguf/` i `localai/`
## Ostatnia rozmowa (advisory — nie wdrożone w repo)
Użytkownik pytał o:
1. **Wystawienie endpointu przez domenę** — reverse proxy (Caddy/nginx) + DNS + HTTPS; LocalAI za proxy na `127.0.0.1:PORT`.
2. **API token** — opcja A: `LOCALAI_API_KEY` (legacy, pełny admin); opcja B: `LOCALAI_AUTH=true` + `LOCALAI_BASE_URL` (konta użytkowników).
Brak w repo: `Caddyfile`, rozdział proxy/TLS, wpis `LOCALAI_API_KEY` w `docker-compose.yml`.
## Następny agent — zacznij od
1. Przeczytaj [BACKLOG.md](BACKLOG.md) — sekcja **P0**.
2. Napraw przekazanie `LOCALAI_API_KEY` do kontenera i zweryfikuj auth po restarcie.
3. Uzgodnij z użytkownikiem port **8070** vs **8080** w dokumentacji.
4. Po zakończeniu pobierania modelu — test chat w UI i `curl /v1/chat/completions`.
## Transkrypt sesji
Pełna historia rozmowy (Cursor):
`/home/tomasz-syn-grzegorza/.cursor/projects/home-tomasz-syn-grzegorza/agent-transcripts/f1d1befe-e1e0-44e8-8a40-1f175e6df09c/f1d1befe-e1e0-44e8-8a40-1f175e6df09c.jsonl`
+144
View File
@@ -0,0 +1,144 @@
# Dwa adresy IP na serwerze domowym — poradnik krok po kroku
*Dla hosta GMKtec K11 (`gmktec-k11`), Ubuntu 26.04, dwie karty Ethernet.*
---
## Po co dwa IP?
Serwer ma **dwie fizyczne karty sieciowe**. Docelowo (gdy oba kable podłączone):
| Karta | Adres | Do czego |
|-------|-------|----------|
| **eno1** | `192.168.100.80` | Internet — brama domyślna `192.168.100.1` |
| **enp3s0** | `192.168.100.90` | LAN — Portainer, LocalAI |
Skrypt **sam wykrywa**, które porty mają kabel, i pokazuje menu z sensownymi opcjami. Nie musisz zgadywać, który port „działa”.
---
## Zanim zaczniesz
### 1. Podłącz kabel(e)
- **Oba porty** — wybierzesz konfigurację dual (`.80` na eno1 + brama, `.90` na enp3s0).
- **Jeden port** — skrypt zaproponuje wariant pojedynczy (np. tylko enp3s0 z `.90` i bramą).
Sprawdź ręcznie:
```bash
ip -br link show eno1 enp3s0
```
Kolumna stanu: `UP` + link = kabel w porcie.
### 2. Zarezerwuj adresy na routerze
Wyklucz z DHCP: `192.168.100.80` i `192.168.100.90`.
### 3. Plan B
Druga sesja SSH lub konsola fizyczna — po zmianie IP stara sesja może się rozłączyć.
---
## Instalacja — skrypt ze skanem i menu
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/scripts
```
### Podgląd (bez zmian)
```bash
sudo ./configure-static-dual-nic.sh --dry-run
```
Zobaczysz skan interfejsów, domyślną opcję menu i YAML Netplan.
### Właściwe uruchomienie
```bash
sudo ./configure-static-dual-nic.sh
```
Przykład interakcji:
```
=== Skan interfejsów ===
Interfejs Stan Link IPv4
eno1 UP NIE —
enp3s0 UP TAK 192.168.100.2/24
=== Wybierz konfigurację ===
1) Pojedynczy enp3s0: .90+brama (zalecane — usługi Docker)
2) Pojedynczy enp3s0: .80+brama
3) Pojedynczy enp3s0: .80+.90+brama (oba IP na jednej karcie)
0) Anuluj
Wybór [1-3, domyślnie 1]:
```
Gdy **oba** porty mają link:
```
1) Dual: eno1=.80+brama, enp3s0=.90 (zalecane)
2) Dual odwrócony: enp3s0=.80+brama, eno1=.90
```
Skrypt dalej: backup Netplan → zapis → `netplan apply` → weryfikacja IP i ping do routera.
---
## Sprawdzenie, czy działa
```bash
ip -br addr show eno1 enp3s0
ip route show default
ping -c 2 192.168.100.1
```
Usługi (gdy `.90` na enp3s0 lub w profilu „both” na enp3s0):
- Server UI: `http://192.168.100.90:8091`
- LocalAI: `http://192.168.100.90:8070`
---
## Co zrobić, gdy coś poszło nie tak
```bash
sudo ./restore-dhcp-network.sh
ip -br addr
```
SSH może wrócić pod adresem DHCP (np. `192.168.100.2`).
Po rollbacku uruchom ponownie `configure-static-dual-nic.sh` i wybierz inną opcję z menu (np. pojedynczy enp3s0, jeśli eno1 nie współpracuje).
---
## FAQ
**Miałem kabel w eno1 i nie było IP — co się stało?**
Stara wersja skryptu wymuszała bramę na eno1 bez uwzględnienia stanu drugiej karty. Wersja v2 używa poprawnego Netplan (`optional`, `ConfigureWithoutCarrier`) i menu zależnego od kabli.
**Czy muszę restartować serwer?**
Nie — wystarczy `netplan apply` (robi to skrypt).
**Który adres przy port forward na routerze?**
Zwykle **`192.168.100.90`** — sprawdź `ip a`, gdzie faktycznie siedzi `.90`.
---
## Pliki
| Plik | Opis |
|------|------|
| [`scripts/configure-static-dual-nic.sh`](../scripts/configure-static-dual-nic.sh) | Skan + menu + statyczne IP |
| [`scripts/restore-dhcp-network.sh`](../scripts/restore-dhcp-network.sh) | Powrót do DHCP |
| [`NETWORK-STATIC-IP-HANDOFF.md`](NETWORK-STATIC-IP-HANDOFF.md) | Notatki dla agenta |
---
*Ostatnia aktualizacja: 2026-07-04 (skrypt v2 — skan + menu).*
@@ -0,0 +1,94 @@
# Raport wdrożenia — statyczne IP dual NIC
> **Szablon** — uzupełnij po uruchomieniu `configure-static-dual-nic.sh` na hoście.
## Metadane
| Pole | Wartość |
|------|---------|
| Host | `gmktec-k11` |
| Data wdrożenia | _do uzupełnienia_ |
| Wykonał | _do uzupełnienia_ |
| Skrypt | [`scripts/configure-static-dual-nic.sh`](../scripts/configure-static-dual-nic.sh) |
| **Wybrany profil** | _np. dual-eno1, single-enp3s0-90_ |
| **Opis profilu** | _np. eno1=.80+brama, enp3s0=.90_ |
---
## Skan przed apply (z outputu skryptu)
| Interfejs | Link | IPv4 przed |
|-----------|------|------------|
| eno1 | _TAK/NIE_ | __ |
| enp3s0 | _TAK/NIE_ | __ |
---
## Konfiguracja po apply
| Interfejs | IP | Brama |
|-----------|-----|-------|
| eno1 | __ | _tak/nie_ |
| enp3s0 | __ | _tak/nie_ |
---
## Pre-flight
- [ ] Co najmniej jeden interfejs z linkiem
- [ ] `.80` i `.90` zarezerwowane na routerze
- [ ] Backup Netplan utworzony
Ścieżka backupu: `_/etc/netplan/00-installer-config.yaml.bak._________`
---
## Wyniki weryfikacji (wklej output)
### `ip -br addr show eno1 enp3s0`
```
_do uzupełnienia_
```
### `ip route show default`
```
_do uzupełnienia_
```
### Ping
| Test | Wynik |
|------|-------|
| `ping -I <gw_nic> 192.168.100.1` | _OK / FAIL_ |
| `ping 8.8.8.8` | _OK / FAIL_ |
### Usługi
| URL | Kod |
|-----|-----|
| `http://192.168.100.90:8091` (Server UI) | __ |
| `http://192.168.100.90:8070` (LocalAI) | __ |
---
## Problemy i rozwiązania
_do uzupełnienia_
---
## Rollback
- [ ] Nie użyto
- [ ] `restore-dhcp-network.sh` — data: _
---
## Status
- [ ] Wdrożenie zakończone pomyślnie
- [ ] Wymaga follow-up
**Następne kroki:** [`NETWORK-STATIC-IP-HANDOFF.md`](NETWORK-STATIC-IP-HANDOFF.md) — sync IP w dokumentacji.
+109
View File
@@ -0,0 +1,109 @@
# Handoff — statyczne IP (dual NIC) dla agenta kodującego
## Stan: skrypt v2 (skan + menu), wdrożenie przez użytkownika
- [x] [`scripts/configure-static-dual-nic.sh`](../scripts/configure-static-dual-nic.sh) — Netplan **1.2** (`ignore-carrier`, DNS na interfejsie z bramą; bez `networkd:`)
- [x] [`scripts/restore-dhcp-network.sh`](../scripts/restore-dhcp-network.sh) — pomija zepsute backupy; `--force-dhcp`
- [ ] **Użytkownik uruchamia** skrypt na hoście
- [ ] Po apply: uzupełnić [`NETWORK-STATIC-IP-DEPLOYMENT.md`](NETWORK-STATIC-IP-DEPLOYMENT.md) (wybrany profil)
- [ ] Zsynchronizować `192.168.100.5``192.168.100.90` w pozostałych handoffach
Artykuł: [`NETWORK-STATIC-IP-BLOG.md`](NETWORK-STATIC-IP-BLOG.md).
---
## Jak działa skrypt (v2)
1. **Skan** `eno1` / `enp3s0` — stan, link (TAK/NIE), obecny IPv4
2. **Menu CLI** — tylko opcje pasujące do wykrytych kabli
3. **Netplan** — globalne `nameservers`, `optional` / `networkd.ConfigureWithoutCarrier`, brama z `metric: 100`
4. **Weryfikacja** — oczekiwane IP, `networkctl reconfigure`, `ping -I <gw_nic> 192.168.100.1`
### Profile (menu / `--profile`)
| Profil | eno1 | enp3s0 | Brama |
|--------|------|--------|-------|
| `dual-eno1` (dual, zalecane) | `.80` | `.90` | eno1 |
| `dual-enp3s0` | `.90` | `.80` | enp3s0 |
| `single-eno1-80` | `.80` | — | eno1 |
| `single-eno1-both` | `.80` + `.90` | — | eno1 |
| `single-enp3s0-90` | — | `.90` | enp3s0 |
| `single-enp3s0-80` | — | `.80` | enp3s0 |
| `single-enp3s0-both` | — | `.80` + `.90` | enp3s0 |
Menu pokazuje się tylko gdy **co najmniej jeden** interfejs ma link. Żaden link → błąd i `exit 1`.
---
## Uruchomienie
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/scripts
# Interaktywnie (skan + menu):
sudo ./configure-static-dual-nic.sh
# Podgląd YAML (domyślna opcja 1 z menu po skanie):
sudo ./configure-static-dual-nic.sh --dry-run
# Bez menu (agent / automatyzacja):
sudo ./configure-static-dual-nic.sh --non-interactive --profile dual-eno1
```
Rollback (pomija zepsute backupy ze statycznej konfiguracji):
```bash
sudo ./restore-dhcp-network.sh
# lub wymuś czysty DHCP:
sudo ./restore-dhcp-network.sh --force-dhcp
```
---
## Pre-flight
1. Zarezerwuj `.80` i `.90` na routerze (DHCP)
2. Druga sesja SSH lub konsola fizyczna
3. Skrypt tworzy backup: `/etc/netplan/00-installer-config.yaml.bak.<timestamp>`
**Nie zakładaj** sztywnie kabla w `eno1` — skrypt wykrywa link i proponuje właściwy profil.
---
## Weryfikacja po apply
```bash
ip -br addr show eno1 enp3s0
ip route show default
ping -c 2 -I eno1 192.168.100.1 # lub -I enp3s0, zależnie od profilu
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.100.90:8091
```
---
## Troubleshooting
| Problem | Działanie |
|---------|-----------|
| Brak IPv4 na eno1 mimo kabla (stary bug) | v2: `ConfigureWithoutCarrier` + menu; spróbuj `dual-eno1` lub `single-enp3s0-90` jeśli tylko enp3s0 |
| Brak internetu | `ip route`; brama musi być na interfejsie **z kablem** |
| SSH urwane | `ip a` z konsoli; rollback `restore-dhcp-network.sh` |
| Konflikt IP | Zwolnij `.80`/`.90` na routerze |
---
## Follow-up po wdrożeniu
Pliki z `192.168.100.5`**`.90`** (gdy usługi na enp3s0):
- `coding-agent/DOCKER-UI-*.md`, `SWARMUI-*.md`, `PORTAINER-SETUP-TOKEN.md`, `BACKLOG.md`
- `stacks/npmplus/README.md`, `manual-tutorial/06-gpu-fan-control.md`
Gdy wybrano `single-enp3s0-both` lub `single-enp3s0-90` — usługi pod `.90` na enp3s0.
---
## Backlog
- [ ] Wypełnić `NETWORK-STATIC-IP-DEPLOYMENT.md` z wybranym profilem
- [ ] Zaktualizować IP w handoffach
+40
View File
@@ -0,0 +1,40 @@
# Usunięcie Portainer CE
Data: 2026-07-04
Host: `gmktec-k11`
## Powód
Portainer 2.43+ wymaga setup tokena i dokończenia konfiguracji w ~5 minut. Na headless serwerze (SSH-only) powodowało to komunikat timeout i blokadę UI.
## Co usunięto
| Element | Status |
|---------|--------|
| Kontener `portainer` | usunięty |
| Obraz `portainer/portainer-ce:latest` | usunięty |
| Stack w Dockerze | `docker compose --profile portainer down` |
## Co pozostało na dysku (nie ruszane)
```
/data/apps/portainer/
├── data/ # portainer.db, konfiguracja UI
└── admin-password # plik hasła (jeśli istniał)
```
Katalog `stacks/portainer/` w repo — usunięty; zastąpiony przez [`stacks/server-ui/`](../stacks/server-ui/).
## Zamiennik
**Server UI** — port **8091**, [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md)
- Własny panel FastAPI (whitelist stacków)
- Polityka GPU wbudowana w API
- Start/Stop: nagłówek `X-API-Key`
## Powiązane
- [`PORTAINER-SETUP-TOKEN.md`](PORTAINER-SETUP-TOKEN.md) — archiwum procedur Portainer
- [`stacks/server-ui/README.md`](../stacks/server-ui/README.md)
- [`DOCKGE-REMOVAL.md`](DOCKGE-REMOVAL.md)
+225
View File
@@ -0,0 +1,225 @@
# Portainer — setup token (archiwum)
> **Portainer usunięty 2026-07-04.** Aktualny Docker UI: [Server UI](../stacks/server-ui/README.md) na porcie **8091**.
Od Portainer **2.43** (w tym `portainer-ce:latest`) przy **pierwszej** konfiguracji panelu potrzebny jest **setup token** — jednorazowy kod z logów kontenera. Chroni to świeżą instalację przed przejęciem przez kogoś innego w sieci LAN.
**To nie jest** token API z menu użytkownika (ten tworzysz później, po zalogowaniu).
---
## Czy mnie to dotyczy?
| Sytuacja | Co robić |
|----------|----------|
| Logujesz się już **loginem i hasłem** | Setup token **nie** jest potrzebny |
| W przeglądarce widzisz pole **Setup token** | Potrzebujesz kodu z logów (poniżej) |
| Plik `/data/apps/portainer/data/portainer.db` **istnieje** | Instancja została już zainicjowana — zwykle logujesz się hasłem, nie tokenem |
Sprawdzenie na serwerze (SSH):
```bash
ls -la /data/apps/portainer/data/portainer.db
```
Jeśli plik jest — konto admina mogło zostać utworzone. Jeśli nie pamiętasz hasła, patrz sekcja [Reset od zera](#reset-od-zera-ostrożnie).
---
## Jak znaleźć setup token (krok po kroku)
### 1. Upewnij się, że kontener działa
```bash
docker ps --filter name=portainer
```
Powinien być **Up**.
### 2. Odczytaj token z logów
Na maszynie z Dockerem (ten sam host co Portainer):
```bash
docker logs portainer 2>&1 | grep setup_token
```
Szukaj linii w stylu:
```text
setup_token=AbCdEf123456...
```
**Kopiujesz tylko część po znaku `=`** (bez `setup_token=`).
Jeśli `grep` nic nie zwraca:
```bash
docker logs portainer 2>&1 | less
```
W `less` wciśnij `/` i wpisz `setup_token`, Enter.
### 3. Wpisz token w przeglądarce
1. Otwórz **http://192.168.100.90:9000**
2. Wklej **Setup token**
3. Ustaw **hasło administratora** (minimum **12 znaków**)
4. Zatwierdź i wybierz środowisko **Local** (Docker na tym komputerze)
### 4. Ważne ograniczenia czasowe
- Token jest **jednorazowy** — po udanym setupie nie musisz go zapisywać.
- Masz około **5 minut** od startu kontenera na dokończenie konfiguracji. Po tym czasie token może wygasnąć.
- Jeśli minęło za dużo czasu: zrestartuj kontener i **od razu** odczytaj nowy token:
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/portainer
docker compose --profile portainer restart portainer
sleep 3
docker logs portainer 2>&1 | grep setup_token
```
---
## Gdy token „nie działa”
| Problem | Rozwiązanie |
|---------|-------------|
| „Invalid setup token” | Ponownie `docker logs portainer \| grep setup_token` — literówka, stary token |
| Minęło > 5 min | Restart kontenera (jak wyżej), nowy token z logów |
| Panel mówi, że admin już istnieje | **Nie** szukaj tokena — loguj się hasłem ustawionym wcześniej |
| Brak linii `setup_token` w logach | Możliwe, że setup już zakończony (`portainer.db` istnieje) lub starsza wersja obrazu |
---
## Jak pominąć setup token (tylko zaufana sieć LAN)
Jeśli Portainer działa **wyłącznie** w domowej sieci (`192.168.100.x`) i **nie** jest wystawiony na internet, możesz wyłączyć wymóg tokena.
W [`stacks/portainer/docker-compose.yml`](../stacks/portainer/docker-compose.yml) dodaj pod serwisem `portainer`:
```yaml
command: --no-setup-token
```
Potem:
```bash
cd stacks/portainer
docker compose --profile portainer up -d
```
**Uwagi:**
- Działa sensownie przy **nowej** instalacji (brak `portainer.db`). Przy istniejącej bazie ta flaga **nie zmienia** hasła ani konta.
- **Nie używaj** `--no-setup-token`, jeśli port 9000 jest dostępny z internetu.
- To świadome osłabienie zabezpieczenia przy pierwszym starcie — akceptowalne na izolowanym LAN.
---
## Własny token (zamiast losowego z logów)
Przydatne przy skryptach — **nie commituj** tej wartości do git.
```yaml
command: --setup-token TwojTajnyKodNaSetup
```
Ten sam ciąg wpisujesz w polu **Setup token** w przeglądarce.
---
## Hasło admina z pliku (bez tokena)
Rekomendowane przez Portainer przy instalacjach, gdzie nie masz wygodnego dostępu do logów.
### 1. Plik z hasłem (poza repozytorium)
```bash
echo -n 'TwojeSilneHasloMin12Znakow' > /data/apps/portainer/admin-password
chmod 600 /data/apps/portainer/admin-password
```
Plik musi zawierać **samą treść hasła** — bez pustej linii na końcu (`echo -n`).
### 2. Zmiany w `docker-compose.yml`
```yaml
command: --admin-password-file /run/portainer/admin-password
volumes:
# ... istniejące wolumeny ...
- ${DATA_ROOT:-/data}/apps/portainer/admin-password:/run/portainer/admin-password:ro
```
### 3. Pierwszy start
```bash
cd stacks/portainer
docker compose --profile portainer up -d
```
Portainer tworzy konto **admin** z tym hasłem. **Setup token nie jest potrzebny.**
**Uwaga:** przy **istniejącym** `portainer.db` ta flaga **nie nadpisuje** hasła.
---
## Setup token vs Access token (API)
| | Setup token | Access token |
|---|-------------|--------------|
| **Kiedy** | Pierwsza konfiguracja panelu | Po zalogowaniu do UI |
| **Gdzie go wziąć** | `docker logs portainer` | UI → ikona użytkownika → **My account****Access tokens** |
| **W pliku `.env` w repo** | Nie | Nie |
| **Do czego** | Jednorazowe utworzenie admina | Wywołania API Portainera (skrypty) |
W [`stacks/portainer/.env`](../stacks/portainer/.env) **nie ma** żadnego tokena — to normalne.
---
## Reset od zera (ostrożnie)
Gdy utknąłeś: nie znasz hasła, token wygasł, a setup się nie udał.
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/portainer
docker compose --profile portainer down
mv /data/apps/portainer/data /data/apps/portainer/data.bak.$(date +%Y%m%d)
./scripts/start.sh
docker logs portainer 2>&1 | grep setup_token
```
**Stracisz:** zapisane ustawienia Portainer w UI (endpointy, preferencje). Kontenery Docker (`localai`, `npmplus`, …) **nie są** usuwane — tylko konfiguracja samego Portainera.
Przy reinstalacji możesz od razu dodać `command: --no-setup-token` lub `--admin-password-file` (sekcje wyżej).
---
## Rekomendacja dla RTX1 (tylko LAN, headless)
| Scenariusz | Najprostsza droga |
|------------|-------------------|
| **Domyślnie w repo** | `--admin-password-file` + plik na `/data`**bez tokena, bez limitu 5 min** |
| Masz SSH, setup token | `docker logs portainer \| grep setup_token` → przeglądarka (w ciągu 5 min) |
| Chcesz bez tokena na LAN | `--no-setup-token` przy **czystej** instalacji (nadal ~5 min na hasło w UI) |
Stack w repo: [`stacks/portainer/docker-compose.yml`](../stacks/portainer/docker-compose.yml) używa **`--admin-password-file`**.
---
## Stan na hoście (2026-07-04)
- Naprawiono timeout pierwszej konfiguracji przez `--admin-password-file` w compose.
- Hasło: `/data/apps/portainer/admin-password` (utworzone przy wdrożeniu, **nie** w git).
- Logowanie: użytkownik **admin**, hasło z pliku.
- `portainer.db` istnieje — konto admin utworzone przy starcie z pliku hasła.
---
## Powiązane dokumenty
- [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md)
- [`DOCKER-UI-HANDOFF.md`](DOCKER-UI-HANDOFF.md)
- [`stacks/portainer/README.md`](../stacks/portainer/README.md)
- Oficjalna dokumentacja: [How do I find, skip, or customize my setup token?](https://docs.portainer.io/) (Portainer 2.43+)
+28
View File
@@ -0,0 +1,28 @@
# coding-agent — notatki dla agenta kodującego
Katalog handoff dla kolejnych sesji Cursor pracujących nad [`ubuntu-bare-metal`](../).
## Kolejność czytania
1. **[HANDOFF.md](HANDOFF.md)** — podsumowanie sesji, decyzje, stan runtime
2. **[STATE.md](STATE.md)** — co działa teraz, ścieżki na dysku, kluczowe pliki repo
3. **[CONTROL-PLANE-ENV.md](CONTROL-PLANE-ENV.md)** — jeden `.env` dla gpu-fan + Server UI
4. **[BACKLOG.md](BACKLOG.md)** — priorytetyzowane zadania (P0P3)
5. **[CONVENTIONS.md](CONVENTIONS.md)** — wzorce stacków, sekrety, konwencje
6. **[GITEA-PREP.md](GITEA-PREP.md)** — git/Gitea: pierwszy commit, push, checklist sekretów
## Gitea
Repozytorium zainicjowane lokalnie (`main`). Remote i push — według [GITEA-PREP.md](GITEA-PREP.md) (URL od użytkownika).
## Zasady
- Instrukcje dla użytkownika: **po polsku**. Komendy: **po angielsku**.
- **Nie commituj** `.env`, tokenów API ani haseł — tylko `.env.example` bez sekretów.
- W notatkach w tym katalogu **nie zapisuj** wartości `LOCALAI_API_KEY`, `HF_TOKEN` itd.
- Commity i push **tylko na prośbę** użytkownika.
- Nie edytuj plików planu w `.cursor/plans/`.
## Ostatnia aktualizacja
Sesja: pierwszy import git pod Gitea; Server UI — GPU Fan chart (siatka, linijki, responsywność, wyśrodkowanie). Szczegóły: [GITEA-PREP.md](GITEA-PREP.md).
+102
View File
@@ -0,0 +1,102 @@
# Wdrożenie BGE-Reranker-v2-m3 w LocalAI
Data: 2026-07-01
Host: `gmktec-k11` (RTX 3090 Ti, LocalAI `v4.4.3-gpu-nvidia-cuda-13`, port **8070**)
## Podsumowanie
| Element | Wartość |
|---------|---------|
| Model GGUF | `bge-reranker-v2-m3-FP16.gguf` (~1.1 GB) |
| Źródło | [gpustack/bge-reranker-v2-m3-GGUF](https://huggingface.co/gpustack/bge-reranker-v2-m3-GGUF/resolve/main/bge-reranker-v2-m3-FP16.gguf) |
| Backend | **`llama-cpp`** → automatycznie `cuda13-llama-cpp` na obrazie cuda-13 |
| API id | `bge-reranker-v2-m3-FP16.gguf` |
| Endpoint | `POST /v1/rerank` (format Jina/Cohere-compatible) |
| Status | **OK** — HTTP 200, dokument o pandzie na indeksie 2 ma najwyższy `relevance_score` |
## Problem z importem z galerii (naprawiony)
Wcześniejsza próba importu przez UI LocalAI utworzyła błędny YAML:
```yaml
backend: rerankers # źle dla GGUF
parameters:
model: gpustack/bge-reranker-v2-m3-GGUF/resolve/main/bge-reranker-v2-m3-FP16.gguf # URL zamiast pliku lokalnego
```
Backend `rerankers` służy modelom HuggingFace (transformers), **nie** plikom GGUF. Dla GGUF wymagany jest `llama-cpp` z:
- `reranking: true`
- `known_usecases: [rerank]`
- `parameters.model` = lokalna nazwa pliku `.gguf`
Dodatkowy backend **nie był potrzebny**`cuda13-llama-cpp` był już zainstalowany (ten sam co Gemma chat i BGE-M3 embed).
## Co zrobiono
1. Pobrano GGUF do `/data/apps/localai/models/bge-reranker-v2-m3-FP16.gguf`
2. Zastosowano poprawny YAML (szablon w repo)
3. Dodano skrypt `stacks/localai/scripts/download-reranker.sh`
4. Restart kontenera `localai`
5. Smoke test `/v1/rerank`**200 OK**
## Pliki w repo
| Plik | Rola |
|------|------|
| [`stacks/localai/profiles/bge-reranker-v2-m3-FP16-rerank.yaml.example`](../stacks/localai/profiles/bge-reranker-v2-m3-FP16-rerank.yaml.example) | Szablon YAML |
| [`stacks/localai/scripts/download-reranker.sh`](../stacks/localai/scripts/download-reranker.sh) | Pobieranie GGUF + aplikacja profilu |
## Pliki na hoście (runtime)
```
/data/apps/localai/models/
├── bge-reranker-v2-m3-FP16.gguf # ~1.1 GB
└── bge-reranker-v2-m3-FP16.gguf.yaml # backend llama-cpp, reranking: true
```
## Weryfikacja
```bash
API_KEY=$(grep '^LOCALAI_API_KEY=' stacks/localai/.env | cut -d= -f2)
curl -s http://127.0.0.1:8070/v1/models \
-H "Authorization: Bearer $API_KEY"
# powinien zawierać bge-reranker-v2-m3-FP16.gguf
curl -s http://127.0.0.1:8070/v1/rerank \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "bge-reranker-v2-m3-FP16.gguf",
"query": "What is a panda?",
"top_n": 3,
"documents": [
"hi",
"it is a bear",
"The giant panda (Ailuropoda melanoleuca) is a bear species endemic to China."
]
}'
```
Oczekiwany wynik: `results[0].index == 2` (dokument o pandzie), `relevance_score` najwyższy dla tego indeksu.
## VRAM i SINGLE_ACTIVE_BACKEND
Compose ma `SINGLE_ACTIVE_BACKEND=true` — przy przełączaniu chat → embed → rerank model jest przeładowywany (jak przy embed). Reranker (~1.1 GB) + Gemma (~6.5 GB) **nie mieszczą się naraz** w sensie aktywnego backendu; kolejne wywołania API przełączają model.
Dla pipeline RAG (embed → rerank → chat) klient musi liczyć się z latencją przeładowania (~kilka sekund między typami modeli).
## Integracja z klientem (ai-lawyer / dev)
| Zmienna | Wartość |
|---------|---------|
| `RERANK_MODEL` | `bge-reranker-v2-m3-FP16.gguf` |
| `RERANK_URL` | `https://llm.rtx1.mobile.agency-ai.dev/v1/rerank` (lub LAN `http://192.168.100.5:8070/v1/rerank`) |
| Auth | `Authorization: Bearer <LOCALAI_API_KEY>` |
Payload zgodny z OpenAI/Jina rerank API — pole `documents` to tablica stringów, `query` to zapytanie, `top_n` opcjonalne.
## Uwaga: uprawnienia plików
Pliki YAML utworzone przez kontener/UI mogą być owned by `root`. Skrypt `download-reranker.sh` przy błędzie `cp` zapisuje YAML przez `docker exec localai` (bez sudo na hoście).
+62
View File
@@ -0,0 +1,62 @@
# Handoff — BGE-Reranker w LocalAI
Dla agenta kodującego. Kontekst wdrożenia: [`RERANKER-DEPLOYMENT.md`](RERANKER-DEPLOYMENT.md).
## Stan: ukończone na hoście
- [x] GGUF pobrany
- [x] YAML `llama-cpp` + `reranking: true`
- [x] Model widoczny w `/v1/models`
- [x] `/v1/rerank` zwraca HTTP 200
## Zadania opcjonalne (backlog)
- [ ] Dodać `RERANK_MODEL` do dokumentacji klienta / `.env.example` dev hosta
- [ ] Smoke test rerank przez publiczną domenę NPMPlus (z zewnątrz LAN — hairpin NAT na RTX1)
- [ ] Rozważyć Q4_K_M zamiast FP16 jeśli VRAM/latencja przełączania modeli jest problemem
- [ ] Dodać rozdział w `manual-tutorial/05-localai-stack.md` o modelach embed + rerank
## Powtórzenie instalacji (nowy host)
```bash
cd stacks/localai
cp .env.example .env # ustaw LOCALAI_API_KEY, LOCALAI_PORT=8070
./scripts/download-reranker.sh
docker compose --profile localai restart localai
# czekaj na readyz, potem curl /v1/rerank (patrz RERANKER-DEPLOYMENT.md)
```
## Czego nie robić
- Nie używać `backend: rerankers` dla GGUF
- Nie instalować osobnego backendu — `cuda13-llama-cpp` wystarczy
- Nie commitować `LOCALAI_API_KEY`
- Nie zmieniać `SINGLE_ACTIVE_BACKEND` bez testu VRAM (chat + embed + rerank sekwencyjnie)
## Kluczowe pliki do edycji w przyszłości
| Plik | Kiedy |
|------|-------|
| `stacks/localai/profiles/bge-reranker-v2-m3-FP16-rerank.yaml.example` | zmiana quant / parametrów rerankera |
| `stacks/localai/scripts/download-reranker.sh` | nowy URL lub wariant Q4 |
| `stacks/localai/coding-agent/STATE.md` | po każdej zmianie runtime |
## API — minimalny przykład (Python)
```python
import requests
resp = requests.post(
"http://127.0.0.1:8070/v1/rerank",
headers={"Authorization": f"Bearer {api_key}"},
json={
"model": "bge-reranker-v2-m3-FP16.gguf",
"query": user_query,
"documents": chunk_texts,
"top_n": 5,
},
timeout=120,
)
resp.raise_for_status()
ranked = resp.json()["results"] # sorted by relevance_score desc
```
@@ -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.
+65
View File
@@ -0,0 +1,65 @@
# Server UI CLI — bugfix (pusty terminal)
**Data:** 2026-07-05
**Status:** naprawione
## Symptom
- Zakładka **CLI** pusta (`cli-terminal-wrap` bez zawartości)
- Przycisk **Połącz ponownie** bez efektu
- Inne zakładki (Stacki, Pliki, GPU Fan) działają z tym samym API Key
- **Sprawdź klucz** zwraca „Klucz poprawny”
## Root cause
Błąd JavaScript w [`stacks/server-ui/static/index.html`](../stacks/server-ui/static/index.html) — `connectCli()`:
```javascript
cliFitAddon = new FitAddon(); // TypeError: FitAddon is not a constructor
```
Bundel UMD [`addon-fit.min.js`](../stacks/server-ui/static/vendor/xterm/addon-fit.min.js) eksportuje **obiekt modułu** `{ FitAddon: class }`, nie samą klasę na `window.FitAddon`.
Wywołanie rzuca wyjątek **przed** `cliTerm.open()` → pusty kontener, WebSocket nigdy nie startuje.
Auth WebSocket (`?api_key=`) nie był przyczyną — HTTP API używa tego samego klucza i działało.
## Fix
1. **`createFitAddon()`** — obsługa obu wariantów UMD:
- `new FitAddon()` (gdy global to klasa)
- `new FitAddon.FitAddon()` (aktualny bundel)
2. **`try/catch`** w `connectCli()` — błąd widoczny w `#cli-status`
3. **Sprawdzenie** `typeof Terminal === 'function'`
4. **Fallback** bez FitAddon: `cols: 80`, `rows: 24`, `#cli-terminal { min-height: 360px }`
5. **Reconnect** po udanym `verifyApiKey()` na zakładce CLI
6. **Czytelne komunikaty** `cliWs.onclose` (kod 1008, zły klucz, CLI disabled)
## Weryfikacja
```bash
# Deploy
rsync -a --exclude '.venv' --exclude '__pycache__' stacks/server-ui/ /opt/server-ui/
sudo systemctl restart server-ui
# WebSocket (wymaga websockets / wscat)
API_KEY=$(sudo grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)
# python -c z websockets → echo hello-cli-test
```
**UI:**
1. `http://<IP>:8091/#cli` + API Key → **Zapisz****Sprawdź klucz**
2. Prompt bash widoczny, `echo test` działa
3. **Połącz ponownie** → nowa sesja OK
4. Zły klucz → komunikat w statusie (nie pusty ekran)
## Pliki zmienione
| Plik | Zmiana |
|------|--------|
| `stacks/server-ui/static/index.html` | createFitAddon, try/catch, WS messages, CSS |
| `manual-tutorial/10-server-ui-cli.md` | troubleshooting pustego CLI |
| `coding-agent/SERVER-UI-CLI.md` | link do tego raportu |
Tutorial użytkownika: [`manual-tutorial/10-server-ui-cli.md`](../manual-tutorial/10-server-ui-cli.md)
+101
View File
@@ -0,0 +1,101 @@
# Server UI — CLI (terminal PTY)
**Data:** 2026-07-05
**Status:** wdrożone (bugfix FitAddon: 2026-07-05 — patrz [SERVER-UI-CLI-BUGFIX.md](SERVER-UI-CLI-BUGFIX.md))
Zakładka **CLI** w panelu Server UI (`:8091`) — interaktywny shell bash w przeglądarce (PTY + xterm.js).
## UI
- Zakładka **CLI** (pierwsza, przed Pliki)
- Hash URL: `http://<host>:8091/#cli`
- Wymaga **API Key** w toolbarze (WebSocket nie obsługuje nagłówka `X-API-Key`)
## Architektura
```mermaid
sequenceDiagram
participant Browser
participant xterm as xterm_js
participant WS as WS_api_cli_ws
participant PTY as cli_pty_py
participant Shell as bash
Browser->>xterm: klawiatura
xterm->>WS: dane terminala
WS->>PTY: zapis master_fd
PTY->>Shell: stdin
Shell->>PTY: stdout/stderr
PTY->>WS: odczyt master_fd
WS->>xterm: output
```
## WebSocket
| Parametr | Wartość |
|----------|---------|
| Ścieżka | `/api/cli/ws` |
| Auth | `?api_key=<API_KEY>` (query string) |
| Protokół I/O | surowe bajty terminala (text/binary frames) |
| Resize | JSON: `{"type":"resize","cols":120,"rows":40}` |
### Przykład (wscat)
```bash
API_KEY=$(sudo grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)
# wymaga: npm i -g wscat
wscat -c "ws://127.0.0.1:8091/api/cli/ws?api_key=${API_KEY}"
```
Bez klucza na bind LAN → połączenie zamknięte z kodem `1008`.
## Konfiguracja
W [`/opt/control-plane/.env`](/opt/control-plane/.env) (opcjonalnie):
| Zmienna | Domyślnie | Opis |
|---------|-----------|------|
| `CLI_ENABLED` | `1` | `0` = WebSocket odrzucany |
| `CLI_SHELL` | `/bin/bash` | Shell PTY (np. `/bin/bash -l`) |
| `CLI_DEFAULT_CWD` | `$HOME` usługi | Katalog startowy |
| `CLI_MAX_SESSIONS` | `5` | Limit równoległych sesji WS |
## Uprawnienia POSIX
Server UI działa jako **`tomasz-syn-grzegorza`** (`server-ui.service`):
- `docker ps`, edycja repo, `/data/apps/` — OK
- `sudo` — interaktywnie (hasło użytkownika w terminalu)
- root-only pliki — odmowa jak w SSH
## Kod
| Plik | Rola |
|------|------|
| [`stacks/server-ui/cli_pty.py`](../stacks/server-ui/cli_pty.py) | PTY + asyncio bridge |
| [`stacks/server-ui/app.py`](../stacks/server-ui/app.py) | WebSocket `/api/cli/ws`, mount `/static` |
| [`stacks/server-ui/static/index.html`](../stacks/server-ui/static/index.html) | Zakładka CLI |
| [`stacks/server-ui/static/vendor/xterm/`](../stacks/server-ui/static/vendor/xterm/) | xterm.js + FitAddon (vendored; UMD: `new FitAddon.FitAddon()`) |
## Bezpieczeństwo
- Pełny shell przez LAN = **wysokie ryzyko** — wymagany `API_KEY` przy `SERVER_UI_HOST=0.0.0.0`
- `CLI_ENABLED=0` jako kill switch
- `CLI_MAX_SESSIONS` ogranicza liczbę równoległych bashów
- Brak whitelisty komend (właściciel serwera)
- Sesja kończy się przy zamknięciu zakładki / rozłączeniu WS (nowy bash przy reconnect)
## Deploy
```bash
sudo rsync -a --exclude '.venv' --exclude '__pycache__' \
stacks/server-ui/ /opt/server-ui/
sudo systemctl restart server-ui
```
Tutorial użytkownika: [`manual-tutorial/10-server-ui-cli.md`](../manual-tutorial/10-server-ui-cli.md)
## Znane problemy / bugfix
Pusty terminal przy działającym API Key: [`SERVER-UI-CLI-BUGFIX.md`](SERVER-UI-CLI-BUGFIX.md)
+84
View File
@@ -0,0 +1,84 @@
# Server UI — File Explorer
**Data:** 2026-07-05
**Status:** wdrożone
Zakładka **Pliki** w panelu Server UI (`:8091`) — przeglądanie systemu plików, edycja tekstu, pełny CRUD.
## UI
- Zakładka **Pliki** (druga, po CLI)
- Hash URL: `http://<host>:8091/#files`
- Wymaga `X-API-Key` (ten sam co reszta panelu przy bind LAN)
## API
Wszystkie endpointy: nagłówek `X-API-Key` (gdy `API_KEY` ustawiony i bind LAN).
| Metoda | Ścieżka | Opis |
|--------|---------|------|
| GET | `/api/files?path=/` | Lista katalogu |
| GET | `/api/files/read?path=...` | Odczyt pliku (UTF-8 lub base64 dla binarnych) |
| PUT | `/api/files/write` | `{"path","content"}` — zapis tekstu |
| POST | `/api/files/mkdir` | `{"path"}` — nowy folder |
| POST | `/api/files/rename` | `{"old_path","new_path"}` |
| DELETE | `/api/files?path=...` | Usuń plik lub pusty katalog |
### Przykłady curl
```bash
API_KEY=$(sudo grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)
curl -s "http://127.0.0.1:8091/api/files?path=/tmp" \
-H "X-API-Key: ${API_KEY}"
curl -s "http://127.0.0.1:8091/api/files/read?path=/tmp/test.txt" \
-H "X-API-Key: ${API_KEY}"
curl -s -X PUT "http://127.0.0.1:8091/api/files/write" \
-H "X-API-Key: ${API_KEY}" \
-H "Content-Type: application/json" \
-d '{"path":"/tmp/test.txt","content":"hello"}'
```
## Konfiguracja
W [`/opt/control-plane/.env`](/opt/control-plane/.env) (opcjonalnie):
| Zmienna | Domyślnie | Opis |
|---------|-----------|------|
| `FILE_EXPLORER_ROOT` | `/` | Korzeń przeglądania |
| `FILE_EXPLORER_MAX_BYTES` | `2097152` | Limit odczytu/zapisu (2 MiB) |
## Uprawnienia POSIX
Server UI działa jako użytkownik **`tomasz-syn-grzegorza`** (`server-ui.service`). Explorer **nie omija** uprawnień systemowych:
- `/home/...`, repo, `/data/apps/...` — zwykle OK
- `/opt/control-plane/.env` (root 600) — odmowa w UI
- pliki root-only — komunikat „Brak uprawnień”
## Kod
| Plik | Rola |
|------|------|
| [`stacks/server-ui/file_explorer.py`](../stacks/server-ui/file_explorer.py) | Logika FS |
| [`stacks/server-ui/app.py`](../stacks/server-ui/app.py) | Endpointy `/api/files*` |
| [`stacks/server-ui/static/index.html`](../stacks/server-ui/static/index.html) | Zakładka Pliki |
## Bezpieczeństwo
- Path traversal: `Path.resolve()` + sprawdzenie względem `FILE_EXPLORER_ROOT`
- Pliki binarne: podgląd base64, zapis zablokowany (415)
- Usuwanie: potwierdzenie w UI (`confirm()`)
- Katalog niepusty: 409 przy DELETE
## Deploy
```bash
sudo bash stacks/server-ui/scripts/install.sh
# lub tylko restart po rsync:
sudo systemctl restart server-ui
```
Tutorial użytkownika: [`manual-tutorial/09-file-explorer.md`](../manual-tutorial/09-file-explorer.md)
+142
View File
@@ -0,0 +1,142 @@
# Server UI — opcje instalacji (Control Plane)
**Data:** 2026-07-05
**Host:** gmktec-k11
**Kontekst:** Użytkownik nie widzi Server UI w `docker ps` — wyjaśnienie runtime + instalator z wyborem native/Docker.
**Powiązane:**
- [`stacks/server-ui/`](../stacks/server-ui/)
- [`manual-tutorial/08-server-ui-install.md`](../manual-tutorial/08-server-ui-install.md)
- [`ADR-001-host-agent-control-plane.md`](ADR-001-host-agent-control-plane.md)
---
## 1. Executive summary
- **Server UI** domyślnie działa jako **systemd** (`server-ui.service`, port 8091) — **nie** jako kontener Docker.
- **gpu-fan** działa wyłącznie jako **host agent** (systemd, port 18090 localhost) — Docker **nie jest obsługiwany** (NVML).
- Nowy skrypt **`install-control-plane.sh`** instaluje oba komponenty z menu: gpu-fan native + Server UI native **lub** Docker.
- Tutorial dla użytkownika: [`08-server-ui-install.md`](../manual-tutorial/08-server-ui-install.md).
---
## 2. Architektura
```mermaid
flowchart TB
subgraph dockerStacks [Docker workloads]
comfyui[comfyui :8188]
localai[localai :8070]
npmplus[npmplus :81]
end
subgraph hostNative [Host systemd]
gpufan[gpu-fan agent :18090]
end
subgraph serverUINative [Server UI native]
suiNative[server-ui :8091]
end
subgraph serverUIDocker [Server UI docker optional]
suiDocker[container server-ui :8091]
end
browser[Browser] --> suiNative
browser -.-> suiDocker
suiNative --> dockerStacks
suiDocker --> dockerStacks
suiNative --> gpufan
suiDocker --> gpufan
gpufan --> gpu[RTX 3090 Ti]
```
---
## 3. Instalator
**Ścieżka:** `stacks/server-ui/scripts/install-control-plane.sh`
| Flaga | Wartości | Domyślnie (-y) |
|-------|----------|----------------|
| `--gpu-fan` | yes, no, skip | yes |
| `--server-ui` | native, docker, skip | native |
**Przykłady:**
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/server-ui
sudo ./scripts/install-control-plane.sh # interaktywne menu
sudo ./scripts/install-control-plane.sh -y # gpu-fan + server-ui native
sudo ./scripts/install-control-plane.sh --gpu-fan=yes --server-ui=docker
```
**Skrypty pomocnicze:**
| Skrypt | Rola |
|--------|------|
| `install-control-plane.sh` | Menu: gpu-fan + server-ui |
| `install.sh` | Tylko Server UI native |
| `install-docker.sh` | Tylko Server UI Docker |
Przy przełączaniu native ↔ Docker instalator wyłącza drugi tryb (systemd disable lub `compose down`).
---
## 4. Klucze API
Jeden plik dla gpu-fan + Server UI:
| Środowisko | Plik | Zmienna |
|------------|------|---------|
| Produkcja (systemd) | `/opt/control-plane/.env` | `API_KEY` |
| Dev / Docker | `stacks/control-plane/.env` | `API_KEY` |
`API_KEY` służy do auth panelu (`X-API-Key`) **i** proxy `/api/gpu-fan/*` do agenta.
```bash
grep ^API_KEY= /opt/control-plane/.env
```
Szczegóły: [`CONTROL-PLANE-ENV.md`](CONTROL-PLANE-ENV.md)
---
## 5. Server UI w Dockerze
| Plik | Opis |
|------|------|
| `Dockerfile` | Python 3.12 + docker CLI + compose plugin |
| `docker-compose.yml` | profil `server-ui`, mount `docker.sock` + repo `/repo` |
| `install-docker.sh` | build, up, generacja `.env` |
**GPU fan z kontenera:** `GPU_FAN_AGENT_URL=http://host.docker.internal:18090` + `extra_hosts: host-gateway`.
**Wymagania:** `REPO_ROOT` w `.env` wskazuje na katalog z `stacks/` (mount read-only do `/repo`).
---
## 6. Weryfikacja
```bash
# Native
systemctl is-active server-ui gpu-fan
curl -s http://127.0.0.1:8091/api/health
# Docker
docker compose --profile server-ui ps
curl -s http://127.0.0.1:8091/api/stacks
```
---
## 7. Checklist wdrożenia
- [x] `Dockerfile`, `docker-compose.yml`, `install-docker.sh`
- [x] `install-control-plane.sh`
- [x] `manual-tutorial/08-server-ui-install.md`
- [x] README server-ui + główny README (rozdział 08)
- [ ] Na produkcji: `sudo ./scripts/install-control-plane.sh` lub `install.sh` / `install-docker.sh`
- [ ] Test GPU Fan proxy po instalacji Docker
---
## 8. Dlaczego gpu-fan bez Docker
Patrz [`stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md`](../stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md) i ADR-001. Instalator wyświetla komunikat i oferuje tylko native.
+173
View File
@@ -0,0 +1,173 @@
# Server UI — edycja portów stacków
**Data:** 2026-07-05
**Host:** gmktec-k11
**Kontekst:** Linki „Otwórz :PORT” w Server UI brały port z `stacks.yaml`, podczas gdy Docker mapował port z osobnego `stacks/<name>/.env`. Wdrożono jeden source of truth w `.env` stacku + edycję w UI.
**Powiązane:**
- [`stacks/server-ui/`](../stacks/server-ui/)
- [`coding-agent/SERVER-UI-ARCHITECTURE-RESEARCH.md`](SERVER-UI-ARCHITECTURE-RESEARCH.md)
- [`coding-agent/CONVENTIONS.md`](CONVENTIONS.md)
---
## 1. Executive summary
Server UI odczytuje i zapisuje port hosta w pliku `stacks/<name>/.env` (np. `LOCALAI_PORT=8070`). Karty stacków mają pole numeryczne i przycisk **Zapisz port**; po zapisie uruchamiany jest `docker compose up -d --force-recreate` gdy stack działa. Nowa usługa wymaga tylko wpisu w `stacks.yaml` z polami `port_env`, `port_default`, `port_editable`.
---
## 2. As-is / to-be
| Aspekt | Przed | Po |
|--------|-------|-----|
| Źródło portu w linku UI | `stacks.yaml``ui_port` (statyczny) | `stacks/<name>/.env``port_env` |
| Edycja portu | ręcznie w `.env` + restart compose | UI + `PATCH /api/stacks/{id}/port` |
| Nowa usługa | hardcode w JS | metadane w `stacks.yaml` |
| NPMPlus | link :81 | read-only (host network) |
```mermaid
flowchart LR
yaml[stacks.yaml port_env metadata]
env[stack .env]
api[Server UI API]
ui[Stack card port input]
compose[docker compose force-recreate]
yaml --> api
env --> api
api --> ui
api -->|PATCH| env
env --> compose
```
---
## 3. Konfiguracja stacków (`stacks.yaml`)
| Stack | `port_env` | `port_default` | `port_editable` |
|-------|------------|----------------|-----------------|
| localai | `LOCALAI_PORT` | 8080 | true |
| comfyui | `COMFYUI_PORT` | 8188 | true |
| vllm | `VLLM_PORT` | 8000 | true |
| npmplus | — | 81 | false |
**Nowa usługa** — dodaj wpis:
```yaml
- id: myservice
name: My Service
compose_dir: myservice
profile: myservice
container: myservice
ui_port: 9000 # fallback gdy brak w .env
port_env: MYSERVICE_PORT
port_default: 9000
port_editable: true
gpu: false
```
W `docker-compose.yml` stacku: `"${MYSERVICE_PORT:-9000}:9000"`.
---
## 4. API
### `GET /api/stacks`
Każdy stack zawiera dodatkowo:
```json
{
"ui_port": 8070,
"port_env": "LOCALAI_PORT",
"port_editable": true,
"port_default": 8080,
"published_port": 8070,
"port_pending_restart": false
}
```
`port_pending_restart`: kontener działa, ale opublikowany port ≠ wartość w `.env`.
### `PATCH /api/stacks/{stack_id}/port`
**Auth:** `X-API-Key` (jak start/stop)
**Body:**
```json
{ "port": 8070, "recreate": true }
```
**Odpowiedź:**
```json
{
"ok": true,
"stack_id": "localai",
"port": 8070,
"port_env": "LOCALAI_PORT",
"requires_restart": false,
"recreated": true,
"running": true
}
```
**Błędy:**
- `400` — stack bez edycji portu (npmplus)
- `409` — port zajęty / poza zakresem 102465535
- `500` — błąd docker compose
**Zarezerwowane porty:** 80, 443, 8090 (gpu-fan), 18090 (gpu-fan agent), `SERVER_UI_PORT` (8091).
---
## 5. Pliki zmienione
| Plik | Zmiana |
|------|--------|
| `stacks/server-ui/stacks.yaml` | `port_env`, `port_default`, `port_editable` |
| `stacks/server-ui/compose_runner.py` | read/write `.env`, walidacja, recreate |
| `stacks/server-ui/app.py` | `PATCH /api/stacks/{id}/port` |
| `stacks/server-ui/static/index.html` | input portu w kartach stacków |
---
## 6. Weryfikacja
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/server-ui
# Lista stacków z portami
curl -s http://127.0.0.1:8091/api/stacks | python3 -m json.tool
# Zmiana portu (wymaga API_KEY z .env)
curl -s -X PATCH http://127.0.0.1:8091/api/stacks/localai/port \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"port":8070,"recreate":true}'
grep LOCALAI_PORT ../localai/.env
```
**UI:** zakładka Stacki → karta LocalAI → pole portu → **Zapisz port** → link „Otwórz” się aktualizuje.
**Prod:** `sudo scripts/install.sh && sudo systemctl restart server-ui`
---
## 7. Uwagi operacyjne
- Po zmianie `LOCALAI_PORT` zaktualizuj **upstream w NPMPlus** ręcznie (proxy host → nowy port).
- `docker compose restart` **nie** zmienia mapowania portów — wymagany recreate (`recreate: true` w API).
- NPMPlus (`network_mode: host`) — port 81 tylko do odczytu w UI.
---
## 8. Checklist wdrożenia
- [x] Metadane portów w `stacks.yaml`
- [x] Backend: odczyt/zapis `.env`, walidacja, recreate
- [x] API `PATCH /port`
- [x] UI: input + Zapisz w kartach
- [x] NPMPlus read-only
- [ ] `install.sh` + restart server-ui na produkcji
- [ ] Test zmiany portu LocalAI na hoście
+69
View File
@@ -0,0 +1,69 @@
# Research: Stability Matrix na gmktec-k11
Data: 2026-07-04
Host: `gmktec-k11` — Ubuntu 26.04 minimized, RTX 3090 Ti 24 GB, **tylko SSH (headless)**
## Czym jest Stability Matrix
[LykosAI/StabilityMatrix](https://github.com/LykosAI/StabilityMatrix) to menedżer pakietów + **GUI desktop** (Avalonia/.NET) do instalacji ekosystemu Stable Diffusion:
- Pakiety: ComfyUI, Forge/A1111, Fooocus, InvokeAI, Kohya, StableSwarmUI, …
- Funkcje: Inference UI, Model Browser (CivitAI/HF), wspólny katalog modeli
- Linux: oficjalnie **AppImage** (`StabilityMatrix-linux-x64.zip`, latest v2.16.1)
- Licencja: AGPL (kod), EULA (binaria)
## Czy działa na Ubuntu?
**Tak — oficjalnie wspierane**, ale jako aplikacja graficzna, nie serwis headless.
| Wymaganie | gmktec-k11 | Ocena |
|-----------|------------|-------|
| OS Linux x64 | Ubuntu 26.04 | OK |
| GPU NVIDIA | RTX 3090 Ti, `nvidia-smi` | OK |
| FUSE (AppImage) | Ubuntu minimized — brak domyślnie | Dołożyć `libfuse2t64` |
| GUI (X11/Wayland) | Serwer SSH, minimized | **Brak** — SM wymaga pulpitu |
| Miejsce na dysku | `/data` ~1 TB | OK |
| Konflikt GPU | LocalAI na 8070 | Jeden duży workload GPU naraz |
### Znane problemy (GitHub Issues)
- Ubuntu 24.04+: `libfuse2t64` wymagane do AppImage
- Instalacja pakietów: czasem brakuje `build-essential`, `python3-dev`, … ([#1454](https://github.com/LykosAI/StabilityMatrix/issues/1454))
- Headless: możliwe z VNC/X11, ale SM nie jest do tego zaprojektowany
- PATH w subprocessach: sporadyczne błędy `nvidia-smi` ([#1577](https://github.com/LykosAI/StabilityMatrix/issues/1577))
## Werdykt
| Pytanie | Odpowiedź |
|---------|-----------|
| Czy można zainstalować na Ubuntu? | **Tak** (AppImage + zależności) |
| Czy ma sens na **tym** hoście? | **Nie** — tylko SSH, bez GUI |
| Zgodność z repo `ubuntu-bare-metal`? | **Słaba** — tutorial 03b zakłada ComfyUI **w Dockerze** |
## Decyzja
**Stability Matrix nie instalujemy.**
Zamiast SM wdrażamy stack **ComfyUI w Dockerze** (`stacks/comfyui/`) — odpowiednik funkcji (generowanie obrazów / ComfyUI), bez GUI-menedżera, zgodny z architekturą repo.
## Gdyby kiedyś (z GUI)
Tylko z VNC/RDP + lightweight desktop lub fizycznym monitorem:
```bash
sudo apt install libfuse2t64
cd /data/apps
wget https://github.com/LykosAI/StabilityMatrix/releases/download/v2.16.1/StabilityMatrix-linux-x64.zip
unzip StabilityMatrix-linux-x64.zip
chmod +x StabilityMatrix.AppImage
./StabilityMatrix.AppImage # wymaga DISPLAY
```
Data Directory: `/data/apps/stability-matrix/` — osobna ścieżka, niezależna od Docker stacks.
## Powiązane
- [`stacks/comfyui/README.md`](../stacks/comfyui/README.md)
- [`COMFYUI-DEPLOYMENT.md`](COMFYUI-DEPLOYMENT.md)
- [`COMFYUI-HANDOFF.md`](COMFYUI-HANDOFF.md)
- [`SWARMUI-REMOVAL.md`](SWARMUI-REMOVAL.md) — poprzedni UI obrazów usunięty
+147
View File
@@ -0,0 +1,147 @@
# Stan systemu i repo
Snapshot na koniec sesji. Weryfikuj na serwerze przed działaniem (`docker ps`, `mountpoint /data`).
## Co działa
| Komponent | Status | Uwagi |
|-----------|--------|-------|
| Docker CE | OK | Root: `/data/docker` |
| NVIDIA driver | OK | ~595.x, `nvidia-smi` na hoście |
| GPU w kontenerze | OK | `--gpus all` testowany wcześniej |
| Mount `/data` | OK | ~1 TB, ext4, fstab UUID |
| vLLM image | OK | `vllm/vllm-openai:latest` pulled |
| vLLM container | STOP | `VLLM_MODEL` puste |
| LocalAI image | OK | `v4.4.3-gpu-nvidia-cuda-13` |
| LocalAI container | RUN | profil `localai`, port host **8070** |
| Server UI | stack w repo | `stacks/server-ui/` :8091 — native (systemd) lub Docker; zakładki: **CLI**, Pliki, Stacki, GPU Fan |
| gpu-fan agent | host systemd | `/opt/gpu-fan` :18090 localhost — **nie w `docker ps`** |
| ComfyUI | stack w repo | `stacks/comfyui/`**nie uruchomiony na hoście** (rozdział 07) |
| Firewall / TLS | — | nie wdrożony (rozdział 07) |
## Ścieżki na `/data`
```
/data/
├── docker/ # obrazy i warstwy Docker
└── apps/
├── vllm/huggingface/ # cache HF (AWQ) — bez pobranego modelu
├── gguf/
│ ├── qwen3.6-27b/ # pod GGUF z katalogu vLLM
│ └── gemma-4-12b/
├── localai/
│ ├── models/ # modele LocalAI (pobieranie w toku)
│ ├── backends/
│ ├── configuration/ # api_keys.json, runtime_settings.json
│ ├── images/
│ └── data/ # auth DB, agents (przyszłe LOCALAI_AUTH)
└── comfyui/ # rozdział 07
├── storage/
├── models/
├── cache/hf-hub/
├── cache/torch-hub/
├── input/
├── output/
├── custom_nodes/
└── workflows/
```
## Pliki `.env` na serwerze (nie w git)
| Plik | Kluczowe ustawienia |
|------|---------------------|
| `/opt/control-plane/.env` | `API_KEY`, `SERVER_UI_*`, `GPU_FAN_*`**jedyny** klucz panelu; instrukcja: [`04a-api-key.md`](../manual-tutorial/04a-api-key.md) |
| `stacks/vllm/.env` | `VLLM_MODEL=` (puste), `MAX_MODEL_LEN=131072`, port 8000 |
| `stacks/localai/.env` | `LOCALAI_PORT=8070`, `LOCALAI_API_KEY` ustawiony (sekret), obraz cuda-13 |
## Porty stacków (Server UI)
| Stack | Zmienna `.env` | Domyślny | Edycja w UI |
|-------|----------------|----------|-------------|
| LocalAI | `LOCALAI_PORT` | 8080 | tak |
| ComfyUI | `COMFYUI_PORT` | 8188 | tak |
| vLLM | `VLLM_PORT` | 8000 | tak |
| NPMPlus | — (host network) | 81 | nie (read-only) |
Szczegóły: [`SERVER-UI-PORT-CONFIG.md`](SERVER-UI-PORT-CONFIG.md)
## Control plane (instalacja)
| Komponent | Runtime | Instalator |
|-----------|---------|------------|
| gpu-fan | systemd native only | `install-control-plane.sh` lub `gpu-fan/scripts/install.sh` |
| Server UI | systemd **lub** Docker | `install-control-plane.sh`, `install.sh`, `install-docker.sh` |
Szczegóły: [`SERVER-UI-INSTALL-OPTIONS.md`](SERVER-UI-INSTALL-OPTIONS.md), [`CONTROL-PLANE-ENV.md`](CONTROL-PLANE-ENV.md), [`SERVER-UI-FILE-EXPLORER.md`](SERVER-UI-FILE-EXPLORER.md), tutorial [`08-server-ui-install.md`](../manual-tutorial/08-server-ui-install.md)
## Struktura repo (kluczowe pliki)
```
ubuntu-bare-metal/
├── README.md
├── scripts/setup-data-disk.sh
├── coding-agent/ # ten katalog
├── manual-tutorial/
│ ├── 00-prerequisites.md
│ ├── 01-system-update-and-docker.md
│ ├── 02-nvidia-driver.md
│ ├── 03-nvidia-container-toolkit.md
│ ├── 03b-system-tools.md
│ ├── 04-vllm-stack.md # część A dysk + część B vLLM
│ └── 05-localai-stack.md
│ ├── 06-gpu-fan-control.md
│ └── 07-comfyui-stack.md
└── stacks/
├── vllm/
│ ├── models.catalog.yaml
│ ├── docker-compose.yml
│ ├── profiles/
│ └── scripts/
├── localai/
│ ├── docker-compose.yml
│ └── scripts/
├── comfyui/
│ ├── docker-compose.yml
│ └── scripts/
└── llamacpp/
└── README.md
```
## Szybka weryfikacja (komendy)
```bash
# dysk i docker
mountpoint /data && df -h /data
docker info | grep "Docker Root Dir"
# vLLM
cd ~/cursor/ubuntu-bare-metal/stacks/vllm
docker compose --profile vllm ps
grep VLLM_MODEL .env
# LocalAI (port z .env użytkownika: 8070)
cd ~/cursor/ubuntu-bare-metal/stacks/localai
docker compose --profile localai ps
curl -s http://localhost:8070/readyz
# GPU
nvidia-smi
```
## Znane rozjazdy (docs vs runtime)
| Temat | Repo / docs | Faktycznie na serwerze |
|-------|-------------|------------------------|
| LocalAI port | 8080 w `.env.example`, tutorial 05 | **8070** w `.env` użytkownika |
| LOCALAI_API_KEY | brak w `docker-compose.yml` | ustawiony w `.env`, **może nie działać** w kontenerze |
| vLLM model | katalog + skrypty gotowe | model nie pobrany, stack nie startowany |
## Git / Gitea
- Repo zainicjowane lokalnie w `ubuntu-bare-metal/` (branch `main`)
- Root `.gitignore` — wyklucza `.env`, `.venv`, `upstream/`
- Push na Gitea: czeka na URL remote — [GITEA-PREP.md](GITEA-PREP.md)
## Server UI — wykres GPU Fan (ostatnie zmiany UI)
W `stacks/server-ui/static/index.html`: siatka osi, linijki kreskowe, odstępy etykiet, `aspect-ratio` SVG, wyśrodkowanie w karcie. Tutorial: [06-gpu-fan-control.md](../manual-tutorial/06-gpu-fan-control.md).
+31
View File
@@ -0,0 +1,31 @@
# SwarmUI — usunięte z hosta (2026-07-04)
Użytkownik zrezygnował ze SwarmUI na serwerze `gmktec-k11`.
## Co usunięto
| Element | Status |
|---------|--------|
| Kontener `swarmui` | usunięty |
| Obraz `swarmui:local` | usunięty |
| Sieć `swarmui_default` | usunięta |
| Dane `/data/apps/swarmui/` (~3.3 GB) | usunięte |
| Stack `stacks/swarmui/` (compose, upstream, skrypty) | usunięty z repo |
| Dokumentacja `SWARMUI-*.md` | usunięta |
## Co zostało
- **LocalAI** — główny workload LLM/embed/rerank (`8070`)
- **Server UI** — zarządzanie stackami compose (`8091`).
- **NPMPlus** — reverse proxy (`80/443`)
- Katalog `/data/apps/comfyui/` — pusty placeholder (nie był używany przez SwarmUI stack)
## Portainer
W UI Portainer stack/kontener `swarmui` może jeszcze widnieć jako „orphan” do czasu odświeżenia — kontener nie istnieje (`docker ps -a`).
## Nie odtwarzać bez prośby
- Nie klonować ponownie `stacks/swarmui/`
- Nie tworzyć `/data/apps/swarmui/`
- Port **7801** jest wolny
+114
View File
@@ -0,0 +1,114 @@
# 00 — Wymagania i konwencje
> Przeczytaj ten rozdział przed rozpoczęciem pracy. Nie wymaga wykonywania komend na serwerze.
## Cel
Ustalenie wspólnego kontekstu: jaki sprzęt konfigurujemy, jakie są wymagania wstępne i jak czytać kolejne rozdziały tutoriala.
## Sprzęt i system
| Parametr | Wartość |
|----------|---------|
| Hostname | `gmktec-k11` |
| System | Ubuntu 26.04 LTS (`resolute`) |
| Kernel | 7.0.x (aktualizowany przez `apt upgrade`) |
| GPU do AI | NVIDIA GeForce RTX 3090 Ti |
| Użytkownik | `tomasz-syn-grzegorza` (grupa `sudo`) |
## Wymagania wstępne
- Dostęp SSH lub fizyczna konsola do serwera
- Użytkownik z uprawnieniami `sudo`
- Stabilne połączenie internetowe
- Czysta instalacja Ubuntu minimized (bez wcześniejszej konfiguracji serwera)
**Ubuntu minimized** — nie ma edytorów (`nano`/`vim`), `rsync`, `parted`, `jq`. Doinstalowujemy je w rozdziale [03b — Narzędzia bazowe](03b-system-tools.md) przed konfiguracją dysku i vLLM.
## Konwencje w tutorialu
### Język
- **Opisy i wyjaśnienia** — po polsku
- **Komendy, nazwy pakietów, ścieżki, zmienne** — po angielsku (jak w systemie Linux)
### Format komend
```bash
# komentarz — wyjaśnienie co robi komenda
sudo apt update
```
- Komendy z prefiksem `sudo` wymagają uprawnień administratora
- `$USER` oznacza aktualnie zalogowanego użytkownika — nie zamieniaj ręcznie
- Bloki oznaczone **Opcjonalnie** możesz pominąć przy pierwszym przejściu
### Oznaczenia w tekście
| Oznaczenie | Znaczenie |
|------------|-----------|
| **Weryfikacja** | Sprawdź wynik przed przejściem dalej |
| **Troubleshooting** | Rozwiązanie typowych problemów |
| **Uwaga** | Ważna informacja — przeczytaj przed wykonaniem |
| **Następny krok** | Link do kolejnego rozdziału |
### Kolejność rozdziałów
Nie pomijaj rozdziałów i nie zmieniaj kolejności. Każdy etap buduje na poprzednim:
1. Aktualizacja systemu + Docker CE
2. Sterowniki NVIDIA
3. NVIDIA Container Toolkit (GPU w kontenerach)
4. vLLM
5. ComfyUI
6. Firewall i hardening
## Czego NIE instalujemy w kroku 01
W pierwszym rozdziale **świadomie pomijamy**:
- Sterowniki NVIDIA (`nvidia-driver-*`)
- NVIDIA Container Toolkit (`nvidia-ctk`)
- vLLM, ComfyUI i jakiekolwiek compose stacki
- Konfigurację firewalla
Te elementy pojawią się w kolejnych rozdziałach we właściwej kolejności.
## Przydatne komendy diagnostyczne (bez zmian w systemie)
Możesz je uruchomić teraz, żeby potwierdzić punkt startowy:
```bash
# wersja systemu
lsb_release -a
# kernel i architektura
uname -a
# miejsce na dysku
df -h /
# pamięć RAM
free -h
# uprawnienia sudo
groups
# czy Docker jest już zainstalowany (powinno być puste)
which docker
docker --version 2>/dev/null || echo "Docker not installed"
# GPU wykryte przez PCI (sterownik może jeszcze nie być zainstalowany)
lspci | grep -i nvidia
```
Oczekiwany stan przed rozdziałem 01:
- Ubuntu 26.04 LTS
- Użytkownik w grupie `sudo`
- Docker **nie** zainstalowany
- `nvidia-smi` **niedostępne** (to normalne — sterownik w rozdziale 02)
## Następny krok
→ [01 — Aktualizacja systemu i instalacja Docker CE](01-system-update-and-docker.md)
@@ -0,0 +1,474 @@
# 01 — Aktualizacja systemu i instalacja Docker CE
> **Cel rozdziału:** zaktualizować świeży Ubuntu 26.04 LTS i zainstalować Docker Engine z oficjalnego repozytorium Docker Inc. (nie `docker.io` z Ubuntu).
**Szacowany czas:** 1020 minut (zależy od liczby pakietów do aktualizacji)
**Wymagania:** rozdział [00 — Wymagania i konwencje](00-prerequisites.md)
---
## Spis treści
1. [Weryfikacja punktu startowego](#1-weryfikacja-punktu-startowego)
2. [Aktualizacja systemu](#2-aktualizacja-systemu)
3. [Instalacja Docker CE](#3-instalacja-docker-ce)
4. [Konfiguracja post-install](#4-konfiguracja-post-install)
5. [Weryfikacja](#5-weryfikacja)
6. [Troubleshooting](#6-troubleshooting)
7. [Czego nie robimy w tym kroku](#7-czego-nie-robimy-w-tym-kroku)
8. [Następny krok](#8-następny-krok)
---
## 1. Weryfikacja punktu startowego
Przed jakimikolwiek zmianami potwierdź stan systemu. Te komendy **nic nie modyfikują**.
```bash
# wersja Ubuntu — oczekiwane: 26.04, codename: resolute
lsb_release -a
# kernel
uname -r
# wolne miejsce na dysku głównym (minimum ~5 GB na upgrade + Docker)
df -h /
# uprawnienia sudo
groups | grep -q sudo && echo "sudo: OK" || echo "sudo: BRAK — wymagane!"
# Docker nie powinien być jeszcze zainstalowany
docker --version 2>/dev/null || echo "Docker: not installed (expected)"
# GPU widoczne w PCI (sterownik jeszcze niepotrzebny)
lspci | grep -i "nvidia"
```
**Oczekiwany wynik:**
- Ubuntu 26.04 LTS (`resolute`)
- Użytkownik w grupie `sudo`
- Docker niezainstalowany
- NVIDIA RTX 3090 Ti widoczna w `lspci`
**Uwaga:** `nvidia-smi` nie działa na tym etapie — to normalne. Sterowniki instalujemy w rozdziale 02.
---
## 2. Aktualizacja systemu
### 2.1 Aktualizacja list pakietów i upgrade
```bash
sudo apt update
sudo apt upgrade -y
```
Pierwszy pełny upgrade na świeżej instalacji może pobrać setki pakietów — poczekaj na zakończenie.
### 2.2 Instalacja pakietów bazowych
Te pakiety są potrzebne do dodania repozytorium Docker i dalszej konfiguracji serwera:
```bash
sudo apt install -y \
ca-certificates \
curl \
gnupg \
lsb-release \
apt-transport-https \
software-properties-common
```
| Pakiet | Po co |
|--------|-------|
| `ca-certificates` | Weryfikacja certyfikatów HTTPS (repo Docker) |
| `curl` | Pobieranie kluczy GPG i plików z internetu |
| `gnupg` | Weryfikacja podpisów pakietów |
| `lsb-release` | Odczyt wersji Ubuntu (codename `resolute`) |
| `apt-transport-https` | Obsługa repozytoriów HTTPS przez apt |
| `software-properties-common` | Narzędzia do zarządzania repozytoriami |
### 2.3 Sprawdzenie, czy wymagany jest restart
```bash
# jeśli plik istnieje — kernel lub libc wymagają restartu
test -f /var/run/reboot-required && cat /var/run/reboot-required || echo "Restart not required"
```
Jeśli restart jest wymagany:
```bash
sudo reboot
```
Po restarcie zaloguj się ponownie i wróć do tego rozdziału od sekcji 3.
**Uwaga:** Na tym systemie `unattended-upgrades` jest domyślnie aktywny — to dobrze dla bezpieczeństwa. Pierwszy pełny upgrade wykonujemy jednak ręcznie, żeby mieć kontrolę nad procesem.
---
## 3. Instalacja Docker CE
Instalujemy Docker z **oficjalnego repozytorium Docker Inc.**, nie z pakietu `docker.io` dostępnego w repozytoriach Ubuntu. Oficjalne repo daje:
- najnowsze wersje Engine,
- plugin `docker compose` (v2),
- plugin `docker-buildx`,
- bezpośrednią ścieżkę aktualizacji (`apt upgrade`).
Dokumentacja: [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
### 3.1 Usunięcie konfliktowych pakietów
Jeśli wcześniej nic nie instalowałeś, ten krok nic nie usunie — ale warto go wykonać dla czystości:
```bash
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
sudo apt remove -y $pkg 2>/dev/null
done
```
### 3.2 Dodanie oficjalnego klucza GPG Docker
```bash
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
```
### 3.3 Dodanie repozytorium Docker (format DEB822)
```bash
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
```
Na Ubuntu 26.04 LTS pole `Suites` powinno wskazywać na `resolute`. Sprawdź:
```bash
grep Suites /etc/apt/sources.list.d/docker.sources
# oczekiwane: Suites: resolute
```
### 3.4 Instalacja pakietów Docker
```bash
sudo apt update
sudo apt install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
```
| Pakiet | Rola |
|--------|------|
| `docker-ce` | Docker Engine (daemon `dockerd`) |
| `docker-ce-cli` | CLI (`docker` command) |
| `containerd.io` | Niskopoziomowy runtime kontenerów |
| `docker-buildx-plugin` | Nowoczesny builder obrazów (BuildKit) |
| `docker-compose-plugin` | `docker compose` — zarządzanie wieloma kontenerami |
---
## 4. Konfiguracja post-install
### 4.1 Włączenie i uruchomienie usługi Docker
```bash
sudo systemctl enable --now docker
```
Sprawdź status:
```bash
sudo systemctl status docker --no-pager
```
Oczekiwane: `Active: active (running)`
### 4.2 Dodanie użytkownika do grupy `docker`
Bez tego każda komenda `docker` wymaga `sudo`:
```bash
sudo usermod -aG docker $USER
```
**Ważne:** Grupa `docker` zostanie aktywna dopiero po ponownym zalogowaniu. Masz dwie opcje:
**Opcja A — re-logowanie (zalecane):**
```bash
# wyloguj się i zaloguj ponownie przez SSH
exit
```
**Opcja B — tymczasowa aktywacja grupy (bez wylogowania):**
```bash
newgrp docker
```
### 4.3 Konfiguracja rotacji logów
Kontenery vLLM i ComfyUI generują dużo logów. Bez rotacji dysk szybko się zapełni.
```bash
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
EOF
sudo systemctl restart docker
```
To ogranicza logi każdego kontenera do 3 plików po 50 MB (max ~150 MB na kontener).
### 4.4 Opcjonalnie: live-restore
Jeśli chcesz, żeby kontenery działały podczas restartu daemona Docker (np. przy aktualizacji):
```bash
sudo tee /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
},
"live-restore": true
}
EOF
sudo systemctl restart docker
```
**Kompromis:** `live-restore` utrudnia debugowanie i może maskować problemy z daemonem. Na serwerze produkcyjnym zwykle warto — na etapie konfiguracji możesz pominąć.
---
## 5. Weryfikacja
Wykonaj wszystkie punkty. Nie przechodź do rozdziału 02, dopóki każdy nie przejdzie.
### 5.1 Wersje Docker i Compose
```bash
docker --version
docker compose version
```
Oczekiwane: Docker version 29.x (lub nowszy), Compose v2.x jako plugin.
### 5.2 Test hello-world
```bash
docker run --rm hello-world
```
Oczekiwane: komunikat `Hello from Docker!` i `status code: 0`.
Jeśli dostajesz `permission denied` — patrz [Troubleshooting §6.1](#61-permission-denied-na-varrundockersock).
### 5.3 Status usługi
```bash
sudo systemctl is-active docker
sudo systemctl is-enabled docker
```
Oczekiwane: `active` i `enabled`.
### 5.4 Informacje o daemonie
```bash
docker info 2>/dev/null | grep -E 'Server Version|Storage Driver|Cgroup Driver|Logging Driver'
```
Oczekiwane m.in.:
- `Storage Driver: overlay2`
- `Logging Driver: json-file`
### 5.5 Test bez sudo (po re-logowaniu / newgrp)
```bash
docker ps
```
Oczekiwane: pusta lista kontenerów, **bez** błędu uprawnień.
### Checklist
- [ ] `docker --version` działa
- [ ] `docker compose version` działa
- [ ] `docker run --rm hello-world` zakończone sukcesem
- [ ] `systemctl status docker` → active (running)
- [ ] `docker ps` działa bez `sudo`
- [ ] `/etc/docker/daemon.json` istnieje z rotacją logów
---
## 6. Troubleshooting
### 6.1 Permission denied na `/var/run/docker.sock`
```
permission denied while trying to connect to the Docker daemon socket
```
**Przyczyna:** Użytkownik nie jest w grupie `docker` lub nie zalogował się ponownie po `usermod`.
**Rozwiązanie:**
```bash
# sprawdź grupy
groups
# jeśli brak "docker":
sudo usermod -aG docker $USER
newgrp docker # lub wyloguj się i zaloguj ponownie
```
### 6.2 `docker-ce has no installation candidate`
**Przyczyna:** Błędny codename w `/etc/apt/sources.list.d/docker.sources`.
**Rozwiązanie:**
```bash
grep Suites /etc/apt/sources.list.d/docker.sources
# musi być: Suites: resolute
# jeśli inny — usuń i dodaj repo ponownie (sekcja 3.3)
sudo rm /etc/apt/sources.list.d/docker.sources
# ... powtórz kroki 3.2 i 3.3
```
### 6.3 Konflikt z `docker.io`
**Objaw:** apt instaluje `docker.io` zamiast `docker-ce`, lub oba się gryzą.
**Rozwiązanie:**
```bash
sudo apt remove -y docker.io docker-doc docker-compose docker-compose-v2
sudo apt autoremove -y
# powtórz sekcję 3.4
```
### 6.4 Błąd pobierania klucza GPG
```
curl: (6) Could not resolve host: download.docker.com
```
**Przyczyna:** Brak internetu lub problem DNS.
**Rozwiązanie:**
```bash
# test połączenia
ping -c 3 download.docker.com
ping -c 3 8.8.8.8
# sprawdź DNS
cat /etc/resolv.conf
```
### 6.5 Daemon nie startuje po `daemon.json`
**Objaw:** `systemctl status docker` → failed.
**Przyczyna:** Błędny JSON w `/etc/docker/daemon.json`.
**Rozwiązanie:**
```bash
# walidacja JSON
python3 -m json.tool /etc/docker/daemon.json
# jeśli błąd składni — przywróć minimalną konfigurację:
sudo tee /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
EOF
sudo systemctl restart docker
```
### 6.6 `hello-world` — image pull failed
```bash
# test dostępu do Docker Hub
docker pull hello-world
# sprawdź logi daemona
sudo journalctl -u docker --no-pager -n 50
```
---
## 7. Czego nie robimy w tym kroku
Świadomie **pomijamy** — pojawią się w kolejnych rozdziałach:
| Element | Rozdział |
|---------|----------|
| Sterowniki NVIDIA (`nvidia-driver-*`) | 02 |
| `nvidia-smi`, CUDA toolkit | 02 |
| NVIDIA Container Toolkit (`nvidia-ctk`) | 03 |
| GPU w kontenerach (`--gpus all`) | 03 |
| vLLM | 04 |
| ComfyUI | 05 |
| UFW / firewall / fail2ban | 06 |
| Katalog `/data` na modele | później |
Docker zainstalowany w tym rozdziale **nie ma dostępu do GPU** — to zamierzone. Najpierw fundament, potem warstwy.
---
## 8. Następny krok
Po przejściu checklisty z sekcji 5:
1. Zgłoś w rozmowie z Cursorem, że krok 01 jest gotowy (lub opisz problemy z Troubleshooting).
2. Przejdź do rozdziału [**02 — Sterowniki NVIDIA**](02-nvidia-driver.md).
---
## Podsumowanie wykonanych zmian
Po ukończeniu tego rozdziału na serwerze powinno być:
- Zaktualizowany system Ubuntu 26.04 LTS
- Zainstalowany Docker CE z oficjalnego repo
- Pluginy `docker compose` i `docker-buildx`
- Użytkownik w grupie `docker`
- Skonfigurowana rotacja logów w `/etc/docker/daemon.json`
- Działający test `hello-world`
+367
View File
@@ -0,0 +1,367 @@
# 02 — Sterowniki NVIDIA
> **Cel rozdziału:** zainstalować sterownik NVIDIA dla RTX 3090 Ti na headless serwerze Ubuntu 26.04, tak aby `nvidia-smi` działało na hoście przed konfiguracją GPU w Dockerze.
**Szacowany czas:** 1530 minut (zależy od pobierania pakietów i ewentualnego DKMS)
**Wymagania:** ukończony rozdział [01 — Aktualizacja systemu i Docker CE](01-system-update-and-docker.md)
---
## Spis treści
1. [Weryfikacja punktu startowego](#1-weryfikacja-punktu-startowego)
2. [Wybór sterownika](#2-wybór-sterownika)
3. [Instalacja sterownika](#3-instalacja-sterownika)
4. [Restart i pierwsze uruchomienie](#4-restart-i-pierwsze-uruchomienie)
5. [Konfiguracja serwerowa (opcjonalna)](#5-konfiguracja-serwerowa-opcjonalna)
6. [Weryfikacja](#6-weryfikacja)
7. [Troubleshooting](#7-troubleshooting)
8. [Czego nie robimy w tym kroku](#8-czego-nie-robimy-w-tym-kroku)
9. [Następny krok](#9-następny-krok)
---
## 1. Weryfikacja punktu startowego
```bash
# Docker z rozdziału 01 — musi działać
docker --version
docker ps
# GPU widoczne w PCI
lspci | grep -i "nvidia"
# nvidia-smi jeszcze nie działa — to normalne
nvidia-smi 2>/dev/null || echo "nvidia-smi: not available (expected)"
# lista dostępnych sterowników
sudo ubuntu-drivers devices
```
**Oczekiwany wynik:**
- RTX 3090 Ti (`GA102`) widoczna w `lspci`
- `nvidia-smi` niedostępne
- `ubuntu-drivers` pokazuje m.in. `nvidia-driver-595-open` jako **recommended**
Sprawdź też Secure Boot (wpływa na DKMS):
```bash
mokutil --sb-state 2>/dev/null || echo "mokutil not available"
```
Na tym serwerze Secure Boot jest wyłączony — nie będzie promptu MOK przy instalacji.
---
## 2. Wybór sterownika
Dla **headless serwera AI** (vLLM + ComfyUI, bez monitora) rekomendujemy wariant **server + open kernel modules**:
| Wariant | Pakiet | Kiedy użyć |
|---------|--------|------------|
| **Rekomendowany** | `nvidia-driver-595-server-open` | Serwer compute, bez GUI, nowoczesne moduły open |
| Alternatywa | `nvidia-driver-595-open` | Jeśli server-open sprawia problemy |
| Nie używamy | `nouveau` | Sterownik open-source — za wolny do AI |
**Dlaczego server-open, a nie desktop `595-open`?**
- Wariant **server** nie ciągnie zbędnych zależności od display managera
- Wariant **open** to aktualna rekomendacja Ubuntu 26.04 dla nowoczesnych GPU
- Sterownik pochodzi z repozytorium Ubuntu (`restricted`) — aktualizuje się przez `apt upgrade` i przeżywa upgrade kernela (DKMS / prebuilt modules)
**Uwaga:** Na hoście **nie instalujemy** pełnego CUDA Toolkit. Kontenery vLLM/ComfyUI dostarczą własne biblioteki CUDA — w rozdziale 03 dodamy tylko NVIDIA Container Toolkit.
**Dual GPU:** W tym systemie jest też iGPU AMD (HawkPoint). Do workloadów AI używamy wyłącznie NVIDIA:
```bash
# zapisz na później — używane w compose stackach
export CUDA_VISIBLE_DEVICES=0
```
---
## 3. Instalacja sterownika
### 3.1 Pakiety pomocnicze
```bash
sudo apt update
sudo apt install -y ubuntu-drivers-common
```
### 3.2 Instalacja sterownika server-open (rekomendowana)
```bash
sudo ubuntu-drivers install --gpgpu nvidia:595-server-open
```
Flaga `--gpgpu` filtruje sterowniki pod obciążenia compute (bez GUI).
Jeśli powyższa komenda zgłosi brak pakietu, użyj bezpośredniej instalacji apt:
```bash
sudo apt install -y nvidia-driver-595-server-open
```
### 3.3 Narzędzia monitorowania (`nvidia-smi`)
Na serwerach headless pakiet `nvidia-utils` czasem nie jest dołączany automatycznie:
```bash
sudo apt install -y nvidia-utils-595-server
```
### 3.4 Alternatywa — sterownik desktop recommended
Jeśli wolisz iść ścieżką „recommended” z `ubuntu-drivers devices`:
```bash
sudo ubuntu-drivers install nvidia:595-open
sudo apt install -y nvidia-utils-595
```
Ta ścieżka też zadziała — ale dla serwera AI preferujemy wariant z sekcji 3.2.
### 3.5 Sprawdzenie zainstalowanych pakietów
```bash
dpkg -l | grep -i nvidia | grep -v linux-firmware
```
Oczekiwane m.in.: `nvidia-driver-595-server-open`, `nvidia-utils-595-server`.
---
## 4. Restart i pierwsze uruchomienie
Sterownik NVIDIA ładuje się do jądra dopiero po restarcie:
```bash
sudo reboot
```
Po restarcie zaloguj się ponownie przez SSH i wróć do sekcji 6 (Weryfikacja).
---
## 5. Konfiguracja serwerowa (opcjonalna)
Te kroki możesz wykonać po pierwszym udanym `nvidia-smi`.
### 5.1 Persistence mode
Zmniejsza opóźnienie przy pierwszym uruchomieniu workloadu GPU (kontenery startują szybciej):
```bash
# włącz persistence mode
sudo nvidia-smi -pm 1
# sprawdź
nvidia-smi | grep -i persistence
```
Aby włączać przy każdym bootcie, utwórz systemd service (opcjonalnie):
```bash
sudo tee /etc/systemd/system/nvidia-persistenced.service <<'EOF'
[Unit]
Description=NVIDIA Persistence Daemon
After=nvidia-persistenced.socket
[Service]
Type=forking
ExecStart=/usr/bin/nvidia-persistenced --user nvidia-persistenced
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now nvidia-persistenced 2>/dev/null || true
```
**Uwaga:** Na Ubuntu 26.04 daemon może już być zarządzany przez pakiet sterownika — jeśli `nvidia-persistenced` działa, nie twórz duplikatu.
### 5.2 Limit mocy GPU (opcjonalnie)
RTX 3090 Ti domyślnie pobiera dużo energii. Na serwerze domowym możesz ograniczyć TDP:
```bash
# sprawdź aktualny limit (450W max dla 3090 Ti)
nvidia-smi -q -d POWER | grep -E "Power Limit|Power Draw"
# przykład: limit 350W (wartość w mW) — dostosuj do swojego PSU
# sudo nvidia-smi -pl 350
```
Pomiń ten krok, jeśli zależy Ci na maksymalnej wydajności inference.
---
## 6. Weryfikacja
Wykonaj wszystkie punkty po restarcie.
### 6.1 `nvidia-smi`
```bash
nvidia-smi
```
Oczekiwane:
- GPU: **NVIDIA GeForce RTX 3090 Ti**
- Driver Version: **595.x**
- CUDA Version: wyświetlona (np. 12.x) — to wersja wspierana przez sterownik, nie osobna instalacja CUDA
- Brak błędów `NVIDIA-SMI has failed`
### 6.2 Wersja modułu jądra
```bash
cat /proc/driver/nvidia/version
```
### 6.3 Pełna lista GPU
```bash
nvidia-smi -L
```
Oczekiwane: jedna karta NVIDIA (GPU 0). iGPU AMD nie pojawia się w `nvidia-smi` — to prawidłowe.
### 6.4 Test obciążenia (krótki)
```bash
nvidia-smi dmon -s pucvmet -d 1 -c 3
```
Powinieneś zobaczyć odczyty temperatury, mocy i wykorzystania GPU.
### 6.5 Docker nadal działa
```bash
docker run --rm hello-world
```
Sterownik NVIDIA nie powinien zakłócić Dockera z rozdziału 01.
### Checklist
- [ ] `nvidia-smi` działa bez błędów
- [ ] Widoczna RTX 3090 Ti (24 GB VRAM)
- [ ] Driver Version 595.x
- [ ] `cat /proc/driver/nvidia/version` zwraca wersję
- [ ] Docker nadal działa (`hello-world` OK)
- [ ] (Opcjonalnie) persistence mode włączony
---
## 7. Troubleshooting
### 7.1 `NVIDIA-SMI has failed` po restarcie
**Przyczyna:** Moduł jądra nie załadowany lub konflikt z `nouveau`.
**Diagnostyka:**
```bash
lsmod | grep -E 'nvidia|nouveau'
dmesg | grep -i nvidia | tail -20
```
**Rozwiązanie:**
```bash
# nouveau powinien być wyłączony — jeśli załadowany:
cat /etc/modprobe.d/blacklist-nvidia-nouveau.conf 2>/dev/null
# przeinstaluj sterownik
sudo apt install --reinstall nvidia-driver-595-server-open
sudo reboot
```
### 7.2 DKMS — błąd kompilacji modułu
**Objaw:** Instalacja kończy się błędem DKMS.
**Rozwiązanie:**
```bash
# upewnij się, że masz nagłówki kernela
sudo apt install -y linux-headers-$(uname -r) build-essential dkms
# ponów instalację
sudo apt install --reinstall nvidia-driver-595-server-open
```
### 7.3 Secure Boot — moduł niepodpisany
**Objaw:** Po restarcie brak `nvidia` w `lsmod`, Secure Boot enabled.
Na tym serwerze Secure Boot jest wyłączony. Jeśli włączysz go później:
```bash
mokutil --sb-state
# wymagana rejestracja klucza MOK po instalacji sterownika
sudo reboot # → menu MOK enrollment
```
### 7.4 `ubuntu-drivers install` — GUI nie działa
Na Ubuntu 26.04 zakładka „Additional Drivers” w GUI może być pusta — to znany problem. **Używaj wyłącznie CLI** (ten rozdział).
### 7.5 Po aktualizacji kernela — `nvidia-smi` przestaje działać
Po `apt upgrade` z nowym kernelem wymagany restart:
```bash
test -f /var/run/reboot-required && echo "Reboot required" || echo "OK"
sudo reboot
```
Ubuntu 26.04 na tym sprzęcie używa prebuilt modules (`linux-modules-nvidia-*`) — zwykle nie wymaga ręcznej rekompilacji DKMS.
### 7.6 Zła karta GPU używana przez aplikację
Jeśli w przyszłości pojawi się druga karta NVIDIA, wymuszaj:
```bash
export CUDA_VISIBLE_DEVICES=0
```
Dla RTX 3090 Ti jako jedynej karty NVIDIA w systemie domyślnie jest `GPU 0`.
---
## 8. Czego nie robimy w tym kroku
| Element | Rozdział |
|---------|----------|
| NVIDIA Container Toolkit (`nvidia-ctk`) | 03 |
| GPU w kontenerach Docker (`--gpus all`) | 03 |
| CUDA Toolkit na hoście | niepotrzebne — CUDA w kontenerach |
| vLLM | 04 |
| ComfyUI | 05 |
| Firewall | 06 |
---
## 9. Następny krok
Po przejściu checklisty z sekcji 6:
1. Zgłoś w rozmowie z Cursorem, że krok 02 jest gotowy (lub opisz problemy).
2. Przejdź do rozdziału [**03 — NVIDIA Container Toolkit**](03-nvidia-container-toolkit.md).
---
## Podsumowanie wykonanych zmian
Po ukończeniu tego rozdziału na serwerze powinno być:
- Zainstalowany sterownik `nvidia-driver-595-server-open` (lub `595-open`)
- Działające `nvidia-smi` z RTX 3090 Ti
- Narzędzia `nvidia-utils-595-server`
- Docker z rozdziału 01 nadal sprawny
- (Opcjonalnie) włączony persistence mode
@@ -0,0 +1,362 @@
# 03 — NVIDIA Container Toolkit
> **Cel rozdziału:** skonfigurować Docker tak, aby kontenery miały dostęp do GPU NVIDIA (RTX 3090 Ti) — fundament pod vLLM i ComfyUI.
**Szacowany czas:** 1015 minut
**Wymagania:** ukończone rozdziały [01](01-system-update-and-docker.md) i [02](02-nvidia-driver.md)
---
## Spis treści
1. [Weryfikacja punktu startowego](#1-weryfikacja-punktu-startowego)
2. [Co robi NVIDIA Container Toolkit](#2-co-robi-nvidia-container-toolkit)
3. [Instalacja](#3-instalacja)
4. [Konfiguracja runtime Docker](#4-konfiguracja-runtime-docker)
5. [Test GPU w kontenerze](#5-test-gpu-w-kontenerze)
6. [Składnia GPU dla Compose (podgląd)](#6-składnia-gpu-dla-compose-podgląd)
7. [Weryfikacja](#7-weryfikacja)
8. [Troubleshooting](#8-troubleshooting)
9. [Czego nie robimy w tym kroku](#9-czego-nie-robimy-w-tym-kroku)
10. [Następny krok](#10-następny-krok)
---
## 1. Weryfikacja punktu startowego
```bash
# sterownik NVIDIA z rozdziału 02
nvidia-smi
# Docker z rozdziału 01
docker --version
docker ps
# toolkit jeszcze niezainstalowany
which nvidia-ctk 2>/dev/null || echo "nvidia-ctk: not installed (expected)"
```
**Oczekiwany wynik:**
- `nvidia-smi` pokazuje RTX 3090 Ti, driver 595.x
- Docker działa (`docker ps` bez błędów)
- `nvidia-ctk` niedostępne
---
## 2. Co robi NVIDIA Container Toolkit
Bez tego pakietu Docker **nie widzi GPU** — kontener uruchomi się, ale bez akceleracji CUDA.
Toolkit dostarcza:
| Komponent | Rola |
|-----------|------|
| `nvidia-container-toolkit` | Integracja GPU z container runtime |
| `nvidia-ctk` | CLI do konfiguracji (Docker, containerd) |
| `libnvidia-container` | Biblioteka montująca sterownik i urządzenia GPU do kontenera |
**Jak to działa:**
```mermaid
flowchart LR
host["Host: nvidia-driver + nvidia-smi"]
docker["Docker Engine"]
toolkit["NVIDIA Container Toolkit"]
container["Kontener vLLM / ComfyUI"]
host --> toolkit
docker --> toolkit
toolkit --> container
```
Kontener **nie potrzebuje** własnego sterownika NVIDIA — dziedziczy go z hosta przez toolkit. Obraz musi mieć tylko biblioteki CUDA kompatybilne z wersją sterownika.
**Uwaga:** Nie instalujemy przestarzałego `nvidia-docker2` — został zastąpiony przez Container Toolkit.
---
## 3. Instalacja
### 3.1 Dodanie repozytorium NVIDIA
```bash
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
```
### 3.2 Instalacja pakietu
```bash
sudo apt update
sudo apt install -y nvidia-container-toolkit
```
### 3.3 Sprawdzenie wersji
```bash
nvidia-ctk --version
dpkg -l | grep nvidia-container
```
---
## 4. Konfiguracja runtime Docker
Toolkit musi zarejestrować runtime NVIDIA w Dockerze:
```bash
sudo nvidia-ctk runtime configure --runtime=docker
```
Ta komenda modyfikuje `/etc/docker/daemon.json` — dodaje konfigurację runtime `nvidia`, **zachowując** istniejące ustawienia (np. rotację logów z rozdziału 01).
Sprawdź wynik:
```bash
cat /etc/docker/daemon.json
```
Oczekiwana struktura (przykład — Twoja może wyglądać nieco inaczej):
```json
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
},
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
}
}
```
Zrestartuj Docker:
```bash
sudo systemctl restart docker
```
Sprawdź, że daemon wstał:
```bash
sudo systemctl status docker --no-pager
```
---
## 5. Test GPU w kontenerze
### 5.1 Podstawowy test — `nvidia-smi` w kontenerze
```bash
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
```
**Oczekiwany wynik:**
- Ten sam GPU: NVIDIA GeForce RTX 3090 Ti
- Driver Version: 595.x (z hosta)
- Brak błędów `could not select device driver`
Pierwsze uruchomienie pobierze obraz CUDA (~100200 MB) — to normalne.
### 5.2 Test z jednym GPU (explicit)
Na tym serwerze jest jedna karta NVIDIA, ale warto od razu testować jawne przypisanie:
```bash
docker run --rm --gpus '"device=0"' nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi -L
```
Oczekiwane: `GPU 0: NVIDIA GeForce RTX 3090 Ti`
### 5.3 Test bez GPU (kontrolny)
```bash
docker run --rm nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
```
Oczekiwane: błąd — kontener bez `--gpus` nie widzi GPU. To potwierdza, że GPU nie jest przypadkowo dostępne dla każdego kontenera.
---
## 6. Składnia GPU dla Compose (podgląd)
W rozdziałach 04 (vLLM) i 05 (ComfyUI) użyjemy `docker compose` z GPU. Podgląd składni:
```yaml
services:
vllm:
image: vllm/vllm-openai:latest
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
```
Alternatywnie (prostsza składnia, działa w Compose v2):
```yaml
services:
comfyui:
image: yanwk/comfyui-boot:latest
gpus: all
```
Na razie **nie twórz** tych plików — to tylko podgląd na przyszłość.
---
## 7. Weryfikacja
### Checklist
- [ ] `nvidia-ctk --version` zwraca wersję
- [ ] `/etc/docker/daemon.json` zawiera runtime `nvidia`
- [ ] `docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi` działa
- [ ] W kontenerze widać RTX 3090 Ti (24 GB)
- [ ] `docker run` **bez** `--gpus` nie widzi GPU (kontrolny test)
- [ ] Docker z rozdziału 01 nadal działa (`hello-world` OK)
### Szybki test końcowy
```bash
docker run --rm hello-world
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
```
---
## 8. Troubleshooting
### 8.1 `could not select device driver "" with capabilities: [[gpu]]`
**Przyczyna:** Runtime NVIDIA nie skonfigurowany lub Docker nie zrestartowany.
**Rozwiązanie:**
```bash
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
docker info | grep -i nvidia
```
### 8.2 `nvidia-smi` działa na hoście, ale nie w kontenerze
**Diagnostyka:**
```bash
# na hoście
nvidia-smi
# sprawdź runtime w docker info
docker info 2>/dev/null | grep -A5 -i runtime
# logi toolkit
sudo journalctl -u docker --no-pager -n 30
```
**Rozwiązanie:**
```bash
sudo apt install --reinstall nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
```
### 8.3 Konflikt w `daemon.json`
**Objaw:** `systemctl restart docker` → failed po edycji `daemon.json`.
**Rozwiązanie:**
```bash
# walidacja JSON
python3 -m json.tool /etc/docker/daemon.json
# jeśli błąd składni — przywróć minimalną konfigurację i skonfiguruj ponownie
sudo tee /etc/docker/daemon.json <<'EOF'
{
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
}
}
EOF
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
```
### 8.4 `apt update` — konflikt Signed-By
**Objaw:** Błąd repozytorium przy `apt update` po dodaniu NVIDIA repo.
**Rozwiązanie:**
```bash
# usuń i dodaj repo ponownie (sekcja 3.1)
sudo rm /etc/apt/sources.list.d/nvidia-container-toolkit.list
# ... powtórz kroki 3.1 i 3.2
```
### 8.5 Obraz CUDA — `pull` failed / timeout
```bash
# test połączenia z registry
docker pull nvidia/cuda:12.6.0-base-ubuntu24.04
# alternatywny lekki obraz testowy
docker run --rm --gpus all nvidia/cuda:12.2.0-base-ubuntu22.04 nvidia-smi
```
### 8.6 `CUDA Version` w kontenerze vs na hoście
Na hoście `nvidia-smi` może pokazywać CUDA 13.2, a obraz testowy CUDA 12.6 — to **normalne**. Kontener używa sterownika hosta; wersja CUDA w obrazie to maksymalna wersja bibliotek w kontenerze, nie sterownik.
---
## 9. Czego nie robimy w tym kroku
| Element | Rozdział |
|---------|----------|
| vLLM | 04 |
| ComfyUI | 05 |
| Pobieranie modeli LLM | 04 |
| CUDA Toolkit na hoście | niepotrzebne |
| Firewall | 06 |
| Katalog `/data` na modele | później |
---
## 10. Następny krok
Po przejściu checklisty z sekcji 7:
1. Zgłoś w rozmowie z Cursorem, że krok 03 jest gotowy (lub opisz problemy).
2. Przejdź do rozdziału [**03b — Narzędzia bazowe**](03b-system-tools.md), a następnie [**04 — Dysk + vLLM**](04-vllm-stack.md).
---
## Podsumowanie wykonanych zmian
Po ukończeniu tego rozdziału na serwerze powinno być:
- Zainstalowany `nvidia-container-toolkit`
- Skonfigurowany runtime NVIDIA w Dockerze
- Działający `docker run --gpus all` z widoczną RTX 3090 Ti
- Gotowa infrastruktura pod vLLM i ComfyUI w kontenerach
+283
View File
@@ -0,0 +1,283 @@
# 03b — Narzędzia bazowe (Ubuntu minimized)
> **Cel rozdziału:** doinstalować minimalny zestaw narzędzi systemowych na Ubuntu minimized — tylko to, co potrzebne na **hoście**. vLLM, ComfyUI i CUDA działają w Dockerze, nie na systemie.
**Szacowany czas:** 5 minut
**Wymagania:** ukończony rozdział [01 — Docker CE](01-system-update-and-docker.md)
**Kiedy wykonać:** przed rozdziałem [04 — dysk + vLLM](04-vllm-stack.md). Można zrobić teraz (kroki 0103 już ukończone).
---
## Spis treści
1. [Filozofia: host vs Docker](#1-filozofia-host-vs-docker)
2. [Co już masz po krokach 0103](#2-co-już-masz-po-krokach-0103)
3. [Instalacja pakietów bazowych](#3-instalacja-pakietów-bazowych)
4. [Python na hoście — co i czego nie](#4-python-na-hoście--co-i-czego-nie)
5. [Git i repozytorium konfiguracyjne](#5-git-i-repozytorium-konfiguracyjne)
6. [Weryfikacja](#6-weryfikacja)
7. [Opcjonalne narzędzia](#7-opcjonalne-narzędzia)
8. [Czego świadomie NIE instalujemy na hoście](#8-czego-świadomie-nie-instalujemy-na-hoście)
9. [Następny krok](#9-następny-krok)
---
## 1. Filozofia: host vs Docker
Ubuntu **minimized** to świadomie odchudzona instalacja — bez edytorów, bez wielu narzędzi deweloperskich, bez GUI. Na serwerze AI trzymamy się zasady:
| Warstwa | Co tam żyje | Przykłady |
|---------|-------------|-----------|
| **Host (128 GB `/`)** | System, Docker, sterowniki, konfiguracja | `git`, `python3`, `nano`, `rsync`, `parted` |
| **Docker (`/data/docker`)** | Obrazy i kontenery | vLLM, ComfyUI, CUDA runtime |
| **Dane (`/data/apps`)** | Modele, checkpointy, cache | Hugging Face, ComfyUI models |
```mermaid
flowchart TB
host["Host Ubuntu minimized"]
docker["Docker Engine"]
vllm["Kontener vLLM — Python + PyTorch + CUDA"]
comfy["Kontener ComfyUI — Python + torch"]
host --> docker
docker --> vllm
docker --> comfy
```
**Nie instalujemy PyTorch, CUDA Toolkit ani vLLM na hoście** — wszystko to jest w kontenerach. Host dostaje tylko narzędzia administracyjne.
---
## 2. Co już masz po krokach 0103
Sprawdź aktualny stan:
```bash
for cmd in git python3 curl wget docker nvidia-smi; do
printf "%-12s " "$cmd"
command -v $cmd 2>/dev/null || echo "BRAK"
done
python3 --version
git --version
```
Na typowym stanie po rozdziałach 0103:
| Narzędzie | Status | Skąd |
|-----------|--------|------|
| `git` | zazwyczaj jest | zależność systemowa / automatyczna |
| `python3` | jest (minimalny) | preinstalowany na Ubuntu |
| `curl`, `wget` | są | rozdział 01 |
| `docker` | jest | rozdział 01 |
| `nvidia-smi` | jest | rozdział 02 |
| `nano`, `vim` | **brak** | minimized |
| `rsync` | **brak** | potrzebny do migracji Docker → `/data` |
| `parted` | **brak** | potrzebny do partycjonowania dysku 1 TB |
| `jq` | **brak** | wygodne testy API vLLM |
| `pip3` | **brak** | celowo — patrz sekcja 4 |
---
## 3. Instalacja pakietów bazowych
Jedna komenda — zestaw dla administracji serwerem i kolejnych rozdziałów tutoriala:
```bash
sudo apt update
sudo apt install -y \
git \
python3 \
python3-venv \
nano \
vim \
jq \
rsync \
parted \
e2fsprogs \
util-linux \
htop \
tmux \
tree \
unzip \
zip \
pciutils \
usbutils \
net-tools \
smartmontools
```
### Po co który pakiet
| Pakiet | Po co na tym serwerze |
|--------|----------------------|
| `git` | Wersjonowanie `ubuntu-bare-metal`, klonowanie configów |
| `python3` | `python3 -m json.tool`, skrypty admin (stdlib) |
| `python3-venv` | Izolowane środowiska Python — jeśli kiedyś własny skrypt |
| `nano` / `vim` | Edycja `.env`, `fstab`, `daemon.json` |
| `jq` | Parsowanie JSON z API vLLM (`curl ... \| jq`) |
| `rsync` | Migracja `/var/lib/docker``/data/docker` (rozdział 04) |
| `parted` | Partycjonowanie dysku 1 TB (rozdział 04) |
| `e2fsprogs` | `mkfs.ext4`, `fsck` — formatowanie `/data` |
| `util-linux` | `lsblk`, `blkid`, `mount` — diagnostyka dysków |
| `htop` | Monitorowanie CPU/RAM |
| `tmux` | Sesje SSH — proces nie ginie po rozłączeniu |
| `tree` | Podgląd struktury `/data` |
| `pciutils` | `lspci` — diagnostyka GPU |
| `smartmontools` | `smartctl` — zdrowie dysków (opcjonalnie, ale przydatne) |
---
## 4. Python na hoście — co i czego nie
### Co mamy
Ubuntu 26.04 minimized dostarcza **Python 3.14** (minimalny) — wystarczy do:
```bash
# formatowanie JSON z API
curl -s http://localhost:8000/v1/models | python3 -m json.tool
# walidacja daemon.json
python3 -m json.tool /etc/docker/daemon.json
```
### Czego NIE robimy na hoście
```bash
# NIE instaluj tego na hoście:
# pip install torch vllm transformers
# apt install nvidia-cuda-toolkit
```
Ubuntu 26.04 blokuje też `pip install` do systemowego Pythona (PEP 668 — „externally managed environment”). To **dobre** — chroni system przed bałaganem.
Jeśli kiedyś potrzebujesz własnego skryptu Python na hoście:
```bash
python3 -m venv ~/venv
source ~/venv/bin/activate
pip install requests # tylko w venv, nie globalnie
deactivate
```
Dla vLLM i ComfyUI — **nie potrzebujesz** venv na hoście.
---
## 5. Git i repozytorium konfiguracyjne
### 5.1 Inicjalizacja repo (jeśli jeszcze nie zrobione)
Repozytorium konfiguracyjne trzymamy na dysku **systemowym** (lekki tekst), nie na `/data`:
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal
git init
git add README.md manual-tutorial/ stacks/
git status
```
**Nie commituj** plików `.env` z tokenami — tylko `.env.example`.
Opcjonalnie `.gitignore`:
```bash
cat > .gitignore <<'EOF'
.env
*.log
__pycache__/
.venv/
EOF
```
### 5.2 Podstawowa konfiguracja git (opcjonalnie)
```bash
git config --global user.name "Twoje Imię"
git config --global user.email "twoj@email.com"
```
Bez tego `git commit` zapyta o autora przy pierwszym commicie.
---
## 6. Weryfikacja
```bash
# pakiety kluczowe
for cmd in git python3 nano vim jq rsync parted lsblk blkid htop tmux; do
printf "%-12s " "$cmd"
command -v $cmd 2>/dev/null || echo "BRAK"
done
# wersje
python3 --version
git --version
jq --version
# test jq
echo '{"status":"ok"}' | jq .
# test rsync
rsync --version | head -1
# test parted
parted --version | head -1
```
### Checklist
- [ ] `nano` lub `vim` działa
- [ ] `jq` formatuje JSON
- [ ] `rsync` dostępny (rozdział 04 — migracja Docker)
- [ ] `parted` i `lsblk` dostępne (rozdział 04 — dysk 1 TB)
- [ ] `git status` w katalogu `ubuntu-bare-metal` działa
---
## 7. Opcjonalne narzędzia
Instaluj tylko jeśli potrzebujesz:
```bash
# kompilacja czegoś ze źródeł (zwykle niepotrzebne na tym serwerze)
# sudo apt install -y build-essential
# monitorowanie GPU w czasie rzeczywistym (wygodniejsze niż watch nvidia-smi)
# sudo apt install -y nvtop
# sieć — ss jest nowocześniejszy niż netstat
ss -tlnp
# synchronizacja czasu (zwykle już działa)
timedatectl status
```
---
## 8. Czego świadomie NIE instalujemy na hoście
| Pakiet / narzędzie | Dlaczego nie |
|--------------------|--------------|
| `nvidia-cuda-toolkit` | CUDA jest w kontenerach Docker |
| `python3-pip` (globalnie) | PEP 668; używaj `venv` lub `jq` |
| `torch`, `vllm`, `transformers` (pip) | Działają w kontenerze vLLM |
| `nodejs`, `npm` | ComfyUI w Dockerze |
| `docker.io` (Ubuntu) | Mamy `docker-ce` z rozdziału 01 |
| GUI / desktop | Serwer headless |
| `snap` pakiety | Niepotrzebna złożoność na serwerze |
---
## 9. Następny krok
→ [04 — Dysk danych 1 TB + vLLM stack](04-vllm-stack.md)
Kolejność:
1. Ten rozdział (03b) — narzędzia bazowe
2. Rozdział 04 część A — dysk 1 TB + migracja Docker
3. Rozdział 04 część B — uruchomienie vLLM
+773
View File
@@ -0,0 +1,773 @@
# 04 — Dysk danych 1 TB + vLLM stack
> **Cel rozdziału:** dodać dysk 1 TB jako `/data` (aplikacje, modele, Docker), a na dysku systemowym 128 GB zostawić wyłącznie Ubuntu. Następnie uruchomić vLLM z mountami na `/data`.
**Szacowany czas:**
- Dysk 1 TB + migracja Docker: 2040 minut
- Pierwsze uruchomienie vLLM: 2060 minut (pobieranie obrazu i modelu)
**Wymagania:** ukończone rozdziały [01](01-system-update-and-docker.md)[03](03-nvidia-container-toolkit.md) oraz [03b — Narzędzia bazowe](03b-system-tools.md)
---
## Spis treści
**Część A — dysk danych**
1. [Architektura dysków](#1-architektura-dysków)
2. [Wykrycie i partycjonowanie dysku 1 TB](#2-wykrycie-i-partycjonowanie-dysku-1-tb)
3. [Montowanie `/data` i wpis w fstab](#3-montowanie-data-i-wpis-w-fstab)
4. [Struktura katalogów na `/data`](#4-struktura-katalogów-na-data)
5. [Przeniesienie Docker data-root na `/data`](#5-przeniesienie-docker-data-root-na-data)
**Część B — vLLM**
6. [Jak działa vLLM (bez UI)](#6-jak-działa-vllm-bez-ui)
7. [Mapowanie ustawień z LM Studio](#7-mapowanie-ustawień-z-lm-studio)
8. [Struktura plików stacku](#8-struktura-plików-stacku)
9. [Przygotowanie stacku (bez modelu)](#9-przygotowanie-stacku-bez-modelu)
10. [Wybór modelu i start](#10-wybór-modelu-i-start)
11. [Test API](#11-test-api)
12. [Tuning po starcie (jeśli OOM)](#12-tuning-po-starcie-jeśli-oom)
13. [Zmiana modelu](#13-zmiana-modelu)
14. [Zarządzanie stackiem](#14-zarządzanie-stackiem)
15. [Weryfikacja](#15-weryfikacja)
16. [Troubleshooting](#16-troubleshooting)
17. [Czego nie robimy w tym kroku](#17-czego-nie-robimy-w-tym-kroku)
18. [Następny krok](#18-następny-krok)
---
# Część A — Dysk danych 1 TB
## 1. Architektura dysków
| Dysk | Rozmiar | Mount | Przeznaczenie |
|------|---------|-------|---------------|
| NVMe systemowy | 128 GB (`nvme1n1`) | `/` | Ubuntu, konfiguracja, repo tutoriala |
| Dysk danych | 1 TB (`nvme0n1`) | `/data` | Docker, modele LLM, ComfyUI, cache |
```mermaid
flowchart TB
subgraph systemDisk ["128GB NVMe — /"]
os["Ubuntu 26.04"]
etc["/etc /boot"]
home["/home"]
repo["cursor/ubuntu-bare-metal"]
end
subgraph dataDisk ["1TB — /data"]
docker["/data/docker — Docker images/volumes"]
vllm["/data/apps/vllm/huggingface"]
comfyui["/data/apps/comfyui/*"]
end
docker --> vllm
docker --> comfyui
```
**Zasada:** Na dysku systemowym nie trzymamy modeli ani obrazów Docker. Wszystko ciężkie idzie na `/data`.
**Uwaga:** Fizycznie zamontuj dysk 1 TB w GMKtec K11 przed wykonaniem sekcji 2. Po podłączeniu zrób reboot lub rescann PCI/NVMe.
---
## 2. Wykrycie i partycjonowanie dysku 1 TB
### 2.1 Wykrycie nowego dysku
```bash
lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,MODEL
```
Oczekiwany układ **po** podłączeniu dysku 1 TB na GMKtec K11:
```
nvme1n1 119G # dysk SYSTEMOWY — NIE DOTYKAĆ
├─nvme1n1p1 1G /boot/efi
└─nvme1n1p2 118G /
nvme0n1 931G # dysk DANYCH 1 TB — ten partycjonujemy
```
**Uwaga:** Na tym modelu GMKtec K11 numeracja bywa odwrotna niż intuicyjnie — `nvme0n1` to 1 TB, `nvme1n1` to 128 GB systemu. Zawsze weryfikuj po `SIZE` i `MOUNTPOINT`, nie po numerze.
**KRYTYCZNE:** Partycjonuj wyłącznie dysk ~1 TB **bez** mountpointu `/`. Nigdy `nvme1n1`.
Zapisz nazwę urządzenia:
```bash
# GMKtec K11 — dysk DANYCH 1 TB (sprawdź lsblk!)
export DATA_DISK=/dev/nvme0n1
export DATA_PART=/dev/nvme0n1p1
echo "DATA_DISK=$DATA_DISK"
echo "DATA_PART=$DATA_PART"
lsblk $DATA_DISK
# weryfikacja: DATA_DISK nie może mieć mountpoint /
lsblk -n -o MOUNTPOINT $DATA_DISK | grep -q '^/$' && echo "BŁĄD: to dysk systemowy!" && exit 1
```
### Szybka instalacja (skrypt)
Jeśli partycja `nvme0n1p1` już istnieje (lub po ręcznym parted), uruchom w **swoim terminalu SSH** (wymaga hasła sudo):
```bash
sudo bash /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/scripts/setup-data-disk.sh
```
Skrypt: formatuje `nvme0n1p1`, montuje `/data`, fstab, katalogi, migracja Docker → `/data/docker`.
---
### 2.2 Partycjonowanie ręczne (jeśli bez skryptu)
Partycja: `/dev/nvme0n1p1` (na GMKtec K11).
```bash
# OBOWIĄZKOWO ustaw obie zmienne przed każdą komendą!
export DATA_DISK=/dev/nvme0n1
export DATA_PART=/dev/nvme0n1p1
# sprawdź jeszcze raz przed zapisem!
lsblk $DATA_DISK
echo "Partycja: $DATA_PART"
sudo parted -s $DATA_DISK mklabel gpt
sudo parted -s $DATA_DISK mkpart primary ext4 0% 100%
sudo partprobe $DATA_DISK
sleep 2
lsblk $DATA_DISK
```
### 2.3 Formatowanie ext4
```bash
sudo mkfs.ext4 -L data1tb $DATA_PART
```
---
## 3. Montowanie `/data` i wpis w fstab
### 3.1 Pobranie UUID (stabilniejsze niż /dev/sdX)
```bash
sudo blkid $DATA_PART
```
Zapisz UUID, np. `UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
### 3.2 Montowanie tymczasowe i test
```bash
sudo mkdir -p /data
sudo mount $DATA_PART /data
df -h /data
```
Oczekiwane: ~1 TB dostępne na `/data`.
### 3.3 Wpis w `/etc/fstab` (montowanie przy bootcie)
```bash
# zamień YOUR-UUID na rzeczywisty UUID z blkid
echo 'UUID=YOUR-UUID /data ext4 defaults,noatime 0 2' | sudo tee -a /etc/fstab
```
**Lepiej edytować ręcznie** — sprawdź składnię:
```bash
sudo nano /etc/fstab
```
Dodaj linię (przykład):
```
UUID=a1b2c3d4-e5f6-7890-abcd-ef1234567890 /data ext4 defaults,noatime 0 2
```
Test fstab (montuje wszystko bez rebootu):
```bash
sudo umount /data
sudo mount -a
df -h /data
```
Jeśli `mount -a` nie zwraca błędu — fstab jest poprawny.
---
## 4. Struktura katalogów na `/data`
```bash
# Docker data-root (własność root)
sudo mkdir -p /data/docker
# Katalogi aplikacji (własność użytkownika — łatwiejszy dostęp)
sudo mkdir -p /data/apps/vllm/huggingface
sudo mkdir -p /data/apps/gguf/{qwen3.6-27b,gemma-4-12b}
sudo mkdir -p /data/apps/localai/{models,backends,configuration,images,data}
sudo mkdir -p /data/apps/comfyui/{models,input,output,custom_nodes}
sudo chown -R $USER:$USER /data/apps
```
Docelowa struktura:
```
/data/
├── docker/ # Docker data-root (obrazy, warstwy, volumes)
└── apps/
├── vllm/
│ └── huggingface/ # cache modeli Hugging Face (AWQ)
├── gguf/ # przyszłe GGUF (llama.cpp)
│ ├── qwen3.6-27b/
│ └── gemma-4-12b/
└── comfyui/ # przygotowane pod rozdział 05
├── models/
├── input/
├── output/
└── custom_nodes/
```
Sprawdź:
```bash
tree -L 3 /data 2>/dev/null || find /data -maxdepth 3 -type d
df -h / /data
```
---
## 5. Przeniesienie Docker data-root na `/data`
Domyślnie Docker trzyma dane w `/var/lib/docker` na dysku systemowym. Przenosimy na `/data/docker`.
### 5.1 Zatrzymanie Docker
```bash
sudo systemctl stop docker
sudo systemctl stop docker.socket 2>/dev/null || true
```
Upewnij się, że nie ma uruchomionych kontenerów:
```bash
docker ps # powinno być puste lub błąd „cannot connect” — OK
```
### 5.2 Kopia istniejących danych Docker
Jeśli już pobierałeś obrazy (np. `hello-world`, `nvidia/cuda`):
```bash
sudo rsync -aHAX --progress /var/lib/docker/ /data/docker/
```
Jeśli `/var/lib/docker` jest pusty lub mało znaczący:
```bash
sudo mkdir -p /data/docker
```
### 5.3 Aktualizacja `/etc/docker/daemon.json`
```bash
sudo python3 -c "
import json
from pathlib import Path
p = Path('/etc/docker/daemon.json')
cfg = json.loads(p.read_text()) if p.exists() else {}
cfg['data-root'] = '/data/docker'
p.write_text(json.dumps(cfg, indent=2) + '\n')
print(p.read_text())
"
```
Oczekiwany fragment:
```json
{
"data-root": "/data/docker",
"log-driver": "json-file",
"log-opts": {
"max-size": "50m",
"max-file": "3"
},
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
}
}
```
### 5.4 Uruchomienie Docker i weryfikacja
```bash
sudo systemctl start docker
docker info | grep "Docker Root Dir"
```
Oczekiwane: `Docker Root Dir: /data/docker`
```bash
docker run --rm hello-world
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
```
### 5.5 Opcjonalnie: zwolnienie miejsca na dysku systemowym
**Dopiero po potwierdzeniu**, że Docker działa z nowego data-root:
```bash
# UWAGA: nieodwracalne — usuwa stare dane Dockera z dysku systemowego
sudo rm -rf /var/lib/docker
```
Sprawdź, że dysk systemowy ma więcej wolnego miejsca:
```bash
df -h /
df -h /data
```
### Checklist — część A (dysk)
- [ ] Dysk 1 TB widoczny w `lsblk`
- [ ] Partycja sformatowana ext4
- [ ] `/data` zamontowany, ~1 TB wolne
- [ ] Wpis w `/etc/fstab` (UUID)
- [ ] `mount -a` bez błędów
- [ ] Katalogi `/data/docker` i `/data/apps/*` utworzone
- [ ] `Docker Root Dir: /data/docker`
- [ ] `hello-world` i test GPU w Dockerze działają
**Nie przechodź do vLLM, dopóki checklista części A nie jest ukończona.**
---
# Część B — vLLM stack
## 6. Jak działa vLLM (bez UI)
vLLM to **serwer API** — nie ma panelu administracyjnego jak LM Studio.
| LM Studio | vLLM na serwerze |
|-----------|------------------|
| Panel UI, suwaki | Plik `.env` + profile + skrypty |
| GGUF Q4 (lmstudio-community) | **Nie w vLLM** — przyszły stack [`stacks/llamacpp/`](../stacks/llamacpp/); interim: AWQ z HF |
| Hugging Face AWQ | Kontener Docker `vllm/vllm-openai` |
| Lokalny chat | HTTP API OpenAI (`/v1/chat/completions`) |
Konfiguracja = zmienne w `.env` przekazywane jako flagi `vllm serve`. Test: `curl` + `jq` lub klient OpenAI.
**Model nie jest pobierany przy instalacji stacku** — katalog `models.catalog.yaml` + `download-model.sh` na żądanie. `VLLM_MODEL` ustawiasz przez profil dopiero gdy jesteś gotowy.
### GGUF z lmstudio-community — nie działa w standardowym vLLM
Linki typu `lmstudio-community/Qwen3.6-27B-GGUF` to pliki **`.gguf`**. Obraz `vllm/vllm-openai` ich nie obsługuje (wymagałby eksperymentalnego `vllm-gguf-plugin` lub osobnego hosta).
| Co chcesz | Co robisz |
|-----------|-----------|
| Q4 jak w LM Studio (GGUF) | `download-model.sh <id-gguf>``/data/apps/gguf/` → później `stacks/llamacpp/` |
| Q4-odpowiednik **teraz** na vLLM | `download-model.sh qwen3.6-27b-awq-vllm` + profil AWQ |
---
## 7. Mapowanie ustawień z LM Studio
Jeśli testowałeś Qwen3.6-27B na Windows (LM Studio / Ollama):
| LM Studio / Ollama | vLLM (`.env` / flagi) |
|--------------------|----------------------|
| Model GGUF Q4 (lmstudio) | Katalog `runtime: llamacpp`**nie** ten stack vLLM |
| Odpowiednik Q4 na vLLM (interim) | `VLLM_MODEL=Qwen/Qwen3.6-27B-Instruct-AWQ`, `QUANTIZATION=awq` |
| K Cache Q4_0 | `KV_CACHE_DTYPE=fp8` |
| V Cache Q4_0 | j.w. — vLLM **nie ma** `Q4_0` (format GGUF/llama.cpp) |
| Context 128K | `MAX_MODEL_LEN=131072` |
| 1 wątek | `MAX_NUM_SEQS=1` |
| GPU layers max | `GPU_MEMORY_UTILIZATION=0.95` |
Alternatywa KV cache (więcej miejsca, bliżej Q4): `KV_CACHE_DTYPE=turboquant_k8v4` — testuj po udanym starcie z `fp8`.
> Później porównamy z kopią ustawień LM Studio na Windows i doprecyzujemy parametry.
---
## 8. Struktura plików stacku
```
stacks/vllm/
├── README.md
├── models.catalog.yaml # GGUF + vLLM AWQ, bez auto-pobierania
├── docker-compose.yml
├── .env.example
├── profiles/
│ ├── _template.env
│ └── qwen3.6-27b-awq-128k.env
└── scripts/
├── list-models.sh
├── download-model.sh
├── switch-model.sh
├── start.sh
└── vllm-entrypoint.sh
```
Dane na dysku 1 TB:
```
/data/apps/vllm/huggingface/ ← cache AWQ (Hugging Face)
/data/apps/gguf/ ← przyszłe GGUF (tworzone przez skrypty)
/data/docker/ ← obrazy Docker (vLLM ~10 GB)
```
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/vllm
```
### Kluczowe elementy `docker-compose.yml`
| Element | Po co |
|---------|-------|
| `profiles: [vllm]` | Serwis nie startuje przypadkowo bez `--profile vllm` |
| `ipc: host` | Shared memory — wymagane przez PyTorch |
| `vllm-entrypoint.sh` | Buduje flagi z `.env` (`QUANTIZATION`, `VLLM_EXTRA_ARGS`) |
| `QUANTIZATION` | Opcjonalne — puste = model pełnej precyzji |
| `VLLM_EXTRA_ARGS` | Flagi per profil (Qwen: `--reasoning-parser qwen3`) |
Domyślne flagi w profilu Qwen (przekazywane przez `VLLM_EXTRA_ARGS`):
| Flaga | Po co |
|-------|-------|
| `--language-model-only` | Bez vision encoder — więcej VRAM na KV cache |
| `--enforce-eager` | Mniej overhead CUDA graphs na 24 GB |
| `--max-num-seqs 1` | Jedna sekwencja naraz (jak 1 wątek w LM Studio) |
| `--kv-cache-dtype fp8` | Kompresja KV cache (`KV_CACHE_DTYPE`) |
| `--max-model-len 131072` | Okno kontekstu 128K (`MAX_MODEL_LEN`) |
---
## 9. Przygotowanie stacku (bez modelu)
### 9.1 Utwórz `.env` z szablonu
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/vllm
cp .env.example .env
cat .env
```
Oczekiwane — `VLLM_MODEL` **pusty**:
```env
DATA_ROOT=/data
VLLM_MODEL=
SERVED_MODEL_NAME=qwen3.6-27b
MAX_MODEL_LEN=131072
MAX_NUM_SEQS=1
GPU_MEMORY_UTILIZATION=0.95
KV_CACHE_DTYPE=fp8
QUANTIZATION=awq
VLLM_EXTRA_ARGS=--language-model-only --enforce-eager --reasoning-parser qwen3
```
Na tym etapie **nic nie pobierasz** — stack jest gotowy do konfiguracji.
### 9.2 Katalog modeli
```bash
./scripts/list-models.sh
```
Pokazuje wpisy z `models.catalog.yaml` i czy pliki są już na dysku (`ON DISK`).
### 9.3 Sprawdź katalogi cache
```bash
mkdir -p /data/apps/vllm/huggingface
df -h /data
```
---
## 10. Wybór modelu i start
### 10.1 Pobierz model (on demand)
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/vllm
# vLLM interim — AWQ (~15 GB) do cache HF
./scripts/download-model.sh qwen3.6-27b-awq-vllm
```
GGUF (na później, pod llama.cpp):
```bash
# ./scripts/download-model.sh qwen3.6-27b-q4-gguf
# ./scripts/download-model.sh gemma-4-12b-q4-gguf
```
### 10.2 Preset Qwen3.6-27B AWQ @ 128K
```bash
./scripts/switch-model.sh qwen3.6-27b-awq-128k
```
Alternatywa — ręcznie:
```bash
cp profiles/qwen3.6-27b-awq-128k.env .env
```
### 10.3 Uruchomienie
```bash
./scripts/start.sh
# lub profil w jednej komendzie:
# ./scripts/start.sh qwen3.6-27b-awq-128k
```
Skrypt sprawdza: `VLLM_MODEL` ustawiony, brak `.gguf`, `/data` zamontowany, Docker działa.
Alternatywa ręczna:
```bash
docker compose --profile vllm pull
docker compose --profile vllm up -d
```
### 10.4 Pierwszy start — czego się spodziewać
1. Pobranie obrazu `vllm/vllm-openai` (~812 GB) → `/data/docker`
2. Pobranie modelu AWQ (~15 GB) → `/data/apps/vllm/huggingface`
3. Ładowanie wag do VRAM — **1030+ minut**
```bash
docker compose --profile vllm logs -f vllm
```
Szukaj:
```
INFO: Uvicorn running on http://0.0.0.0:8000
```
### 10.5 Monitorowanie
```bash
# terminal 1
watch -n 1 nvidia-smi
# terminal 2
watch -n 5 'df -h /data; du -sh /data/apps/vllm/huggingface 2>/dev/null'
```
---
## 11. Test API
Użyj `SERVED_MODEL_NAME` z `.env` (domyślnie `qwen3.6-27b-awq` w presecie).
### 11.1 Lista modeli
```bash
curl -s http://localhost:8000/v1/models | jq .
```
### 11.2 Health check
```bash
curl -s http://localhost:8000/health
```
### 11.3 Chat completion
```bash
curl -s http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen3.6-27b-awq",
"messages": [{"role": "user", "content": "Say hello in one word."}],
"max_tokens": 32
}' | jq .
```
Pole `"model"` musi odpowiadać `SERVED_MODEL_NAME` z `.env`.
---
## 12. Tuning po starcie (jeśli OOM)
Kolejność — zmieniaj w `.env` i restartuj:
```bash
docker compose --profile vllm down
# edytuj .env
docker compose --profile vllm up -d
```
| Krok | Zmiana | Kiedy |
|------|--------|-------|
| 1 | `MAX_MODEL_LEN=98304` lub `65536` | OOM przy ładowaniu lub długim prompcie |
| 2 | `GPU_MEMORY_UTILIZATION=0.90` | Nadal OOM |
| 3 | `KV_CACHE_DTYPE=turboquant_k8v4` | Potrzeba więcej miejsca na KV (bliżej Q4_0) |
---
## 13. Zmiana modelu (A / B na dysku, jeden w VRAM)
Kilka modeli może leżeć na `/data`**aktywny jest tylko jeden** w VRAM. Przełączenie = profil + restart kontenera.
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/vllm
# sprawdź co jest na dysku
./scripts/list-models.sh
# pobierz drugi model (jeśli potrzeba)
# ./scripts/download-model.sh <catalog-id>
# przełącz profil vLLM i zrestartuj
./scripts/switch-model.sh qwen3.6-27b-awq-128k
```
Nowy profil: skopiuj `profiles/_template.env`, dostosuj `VLLM_MODEL`, `QUANTIZATION`, `VLLM_EXTRA_ARGS`.
`start.sh` **odrzuca** `.gguf` w `VLLM_MODEL` — GGUF wymaga przyszłego `stacks/llamacpp/`.
Sprawdź miejsce przed większym modelem:
```bash
df -h /data
du -sh /data/apps/vllm/huggingface
```
---
## 14. Zarządzanie stackiem
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/vllm
docker compose --profile vllm ps
docker compose --profile vllm logs -f vllm
docker compose --profile vllm restart vllm
docker compose --profile vllm down
```
---
## 15. Weryfikacja
### Checklist — cały rozdział 04
**Dysk:**
- [ ] `/data` ~1 TB zamontowany (fstab)
- [ ] `Docker Root Dir: /data/docker`
**vLLM:**
- [ ] `.env` z ustawionym `VLLM_MODEL`
- [ ] `docker compose --profile vllm ps``vllm` running
- [ ] `nvidia-smi` — proces vLLM, VRAM w użyciu
- [ ] `curl http://localhost:8000/v1/models` — JSON z modelem
- [ ] `curl .../v1/chat/completions` — odpowiedź tekstowa
- [ ] Model na `/data/apps/vllm/huggingface`
### Szybki test końcowy
```bash
df -h / /data
docker info | grep "Docker Root Dir"
curl -s http://localhost:8000/v1/models | jq .
du -sh /data/apps/vllm/huggingface /data/docker
```
---
## 16. Troubleshooting
### 16.1 `/data` nie montuje się po reboot
```bash
sudo mount -a
cat /etc/fstab
```
### 16.2 `VLLM_MODEL is empty` lub `.gguf` rejected
```bash
./scripts/list-models.sh
./scripts/download-model.sh qwen3.6-27b-awq-vllm
./scripts/switch-model.sh qwen3.6-27b-awq-128k
```
Jeśli wpisałeś ścieżkę `.gguf` — użyj AWQ (vLLM) lub poczekaj na stack llama.cpp.
### 16.3 OOM na GPU (CUDA out of memory)
Zobacz sekcję 12 (tuning). Typowy zestaw na start po OOM:
```env
MAX_MODEL_LEN=65536
GPU_MEMORY_UTILIZATION=0.90
KV_CACHE_DTYPE=fp8
```
### 16.4 Błąd kwantyzacji AWQ
Upewnij się, że model ma suffix `-AWQ` i `QUANTIZATION=awq` w `.env`.
### 16.5 `connection refused` na :8000
Model jeszcze się ładuje — `docker compose --profile vllm logs -f vllm`.
### 16.6 Wolny pierwszy prefill przy długim kontekście
Normalne przy 128K — pierwsze żądanie z długim promptem trwa dłużej.
### 16.7 Brak miejsca na `/data`
```bash
df -h /data
docker system df
```
---
## 17. Czego nie robimy w tym kroku
| Element | Gdzie |
|---------|-------|
| ComfyUI | Rozdział 06 |
| LocalAI (UI + GGUF) | Rozdział 05 |
| Open WebUI (panel do vLLM) | później, opcjonalnie |
| Pełny Docker llama.cpp / GGUF | placeholder: [`stacks/llamacpp/`](../stacks/llamacpp/) — LocalAI może obsłużyć GGUF |
| Context 262K | start 128K; tuning później |
| Firewall | Rozdział 07 |
---
## 18. Następny krok
Po przejściu checklisty z sekcji 15:
1. Zgłoś w rozmowie z Cursorem, że krok 04 jest gotowy.
2. Przejdź do rozdziału **05 — LocalAI stack** ([`05-localai-stack.md`](05-localai-stack.md)).
---
## Podsumowanie wykonanych zmian
Po ukończeniu tego rozdziału:
- Dysk 1 TB: `/data` (Docker + modele)
- Stack vLLM gotowy bez domyślnego modelu
- Katalog modeli (`models.catalog.yaml`) + skrypty list/download/switch
- Po wyborze: Qwen3.6-27B AWQ (interim Q4), kontekst 128K, KV cache fp8
- GGUF lmstudio — ścieżki na `/data/apps/gguf/`, host llama.cpp planowany
- API OpenAI na porcie 8000
+140
View File
@@ -0,0 +1,140 @@
# 04a — Klucz API Server UI (krok po kroku)
> **Cel:** wpisać poprawny klucz API w panelu `:8091`, żeby działały Start/Stop, CLI, Pliki i GPU Fan.
**Szacowany czas:** 3 minuty
---
## 1. Co to jest klucz API?
To **hasło do panelu** — bez niego możesz tylko **oglądać** status stacków. Z kluczem możesz:
- Start / Stop / Restart stacków
- Terminal **CLI**
- Zakładka **Pliki**
- Sterowanie **GPU Fan**
---
## 2. Gdzie jest JEDYNY plik z kluczem?
Na serwerze produkcyjnym (systemd):
```
/opt/control-plane/.env
```
**Nie używaj** innych plików — stare ścieżki mylą:
| Plik | Status |
|------|--------|
| `/opt/control-plane/.env` | **TAK — używaj tego** |
| `stacks/control-plane/.env` | tylko dev (po sync ma ten sam klucz) |
| `stacks/server-ui/.env` | **NIE** — przestarzałe, ignoruj |
| `GPU_FAN_AGENT_KEY` | **NIE** — usunięte, używaj `API_KEY` |
---
## 3. Wyświetl klucz na serwerze
Zaloguj się na serwer (SSH) i wpisz:
```bash
sudo grep ^API_KEY= /opt/control-plane/.env
```
Przykład wyniku:
```
API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Skopiuj **tylko** część po `=` (bez słowa `API_KEY=`).
Alternatywa — gotowa instrukcja z linkiem:
```bash
bash ~/cursor/ubuntu-bare-metal/stacks/server-ui/scripts/show-api-key.sh
```
---
## 4. Wpisz klucz w panelu (3 kroki)
### Krok A — otwórz panel
W przeglądarce na swoim komputerze:
```
http://<IP-serwera>:8091/
```
Przykład: `http://192.168.100.90:8091/`
### Krok B — wklej klucz
1. U góry strony znajdź pole **API Key**
2. Wklej skopiowany klucz
3. Kliknij **Zapisz**
### Krok C — sprawdź
1. Kliknij **Sprawdź klucz**
2. Powinno pojawić się: **Klucz poprawny** (zielony tekst)
3. Dopiero teraz używaj Start/Stop, CLI itd.
---
## 5. Szybsza metoda — gotowy link
Zamiast kroków BC możesz otworzyć od razu:
```
http://<IP-serwera>:8091/?api_key=TWÓJ_KLUCZ_Z_KROKU_3
```
Klucz zapisze się w przeglądarce automatycznie. Nadal kliknij **Sprawdź klucz** dla pewności.
---
## 6. Jak sprawdzić że wszystko działa
1. Zakładka **Stacki** → wybierz stack → **Start**
2. Brak czerwonego komunikatu `Invalid or missing API key`
3. Status stacku zmienia się na „running”
---
## 7. Błąd „Invalid or missing API key” — checklist
| # | Sprawdź | Co zrobić |
|---|---------|-----------|
| 1 | Kliknąłeś **Zapisz**? | Wklej klucz → **Zapisz****Sprawdź klucz** |
| 2 | Stary klucz w przeglądarce? | F12 → Application → Local Storage → usuń `server-ui-api-key`, odśwież stronę |
| 3 | Klucz z właściwego pliku? | Tylko `sudo grep ^API_KEY= /opt/control-plane/.env` |
| 4 | Panel po restarcie? | `sudo systemctl restart server-ui` |
| 5 | Właściwy proces na porcie? | `ss -tlnp \| grep 8091` — powinno być `/opt/server-ui` |
---
## 8. Po instalacji panelu
Instalator wypisuje gotowy link z kluczem. Jeśli go nie zapisałeś:
```bash
bash ~/cursor/ubuntu-bare-metal/stacks/server-ui/scripts/show-api-key.sh
```
---
## Podsumowanie
| Pytanie | Odpowiedź |
|---------|-----------|
| Gdzie klucz? | `/opt/control-plane/.env``API_KEY` |
| Jak wyświetlić? | `sudo grep ^API_KEY= /opt/control-plane/.env` |
| Jak wpisać w UI? | Pole API Key → Zapisz → Sprawdź klucz |
| Gotowy link? | `http://IP:8091/?api_key=KLUCZ` |
Powrót: [08 — Server UI](08-server-ui-install.md) · [README](README.md)
+315
View File
@@ -0,0 +1,315 @@
# 05 — LocalAI stack
> **Cel rozdziału:** uruchomić [LocalAI](https://github.com/mudler/LocalAI) w Dockerze z GPU (CUDA 13), wbudowanym UI na porcie **8070** i pustym katalogiem modeli. Modele dodasz później (GGUF, galeria).
**Szacowany czas:**
- Pobranie obrazu Docker: 1030 minut
- Start bez modelu: 12 minuty
**Wymagania:** ukończone rozdziały [01](01-system-update-and-docker.md)[04](04-vllm-stack.md) (część A — dysk `/data` zamontowany)
---
## Spis treści
1. [LocalAI vs vLLM](#1-localai-vs-vllm)
2. [Porty i architektura](#2-porty-i-architektura)
3. [Struktura plików stacku](#3-struktura-plików-stacku)
4. [Przygotowanie `.env`](#4-przygotowanie-env)
5. [Clone upstream (opcjonalnie)](#5-clone-upstream-opcjonalnie)
6. [Instalacja obrazu (bez modelu)](#6-instalacja-obrazu-bez-modelu)
7. [Start stacku](#7-start-stacku)
8. [Weryfikacja UI i API](#8-weryfikacja-ui-i-api)
9. [Zarządzanie stackiem](#9-zarządzanie-stackiem)
10. [Troubleshooting](#10-troubleshooting)
11. [Następny krok](#11-następny-krok)
---
## 1. LocalAI vs vLLM
| | vLLM (`stacks/vllm/`) | LocalAI (`stacks/localai/`) |
|--|----------------------|----------------------------|
| UI | Brak (tylko API) | **Wbudowany chat w przeglądarce** |
| Port | 8000 | **8070** (host) → 8080 (kontener) |
| Modele skwantyzowane | AWQ / HF (nie GGUF) | **GGUF, AWQ**, wiele backendów |
| Ten krok | Obraz pobrany, bez modelu OK | Start **bez modelu** — pusty `/models` |
Oba stacki mogą współistnieć na dysku, ale **nie ładuj dużych modeli na GPU równocześnie** (24 GB VRAM).
---
## 2. Porty i architektura
```mermaid
flowchart LR
browser["Przeglądarka :8070"]
curl["curl / OpenAI SDK"]
localai["Kontener localai"]
gpu["RTX 3090 Ti"]
disk["/data/apps/localai/models"]
browser --> localai
curl --> localai
localai --> gpu
localai --> disk
```
| Element | Wartość |
|---------|---------|
| Obraz | `localai/localai:v4.4.3-gpu-nvidia-cuda-13` |
| UI + API (LAN / tunel) | `http://127.0.0.1:8070` lub publicznie przez NPMPlus (rozdział 07) |
| Modele | `/data/apps/localai/models` |
| Docker data | `/data/docker` |
---
## 3. Struktura plików stacku
```
stacks/localai/
├── README.md
├── docker-compose.yml
├── .env.example
├── .gitignore
├── upstream/ # opcjonalny shallow clone (gitignored)
└── scripts/
├── clone-upstream.sh
├── pull.sh
└── start.sh
```
Katalogi na dysku 1 TB (tworzone przez skrypty):
```
/data/apps/localai/
├── models/
├── backends/
├── configuration/
├── images/
└── data/
```
---
## 4. Przygotowanie `.env`
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/localai
cp .env.example .env
cat .env
```
Oczekiwane:
```env
DATA_ROOT=/data
LOCALAI_PORT=8070
LOCALAI_API_KEY=your-secret-key
LOCALAI_IMAGE=localai/localai:v4.4.3-gpu-nvidia-cuda-13
CUDA_VISIBLE_DEVICES=0
DEBUG=false
```
---
## 5. Clone upstream (opcjonalnie)
Shallow clone repozytorium GitHub — **tylko referencja** (przykładowe YAML modeli). Runtime idzie z oficjalnego obrazu Docker, nie z buildu lokalnego.
```bash
./scripts/clone-upstream.sh
```
Powstaje `stacks/localai/upstream/` (ignorowane przez git).
---
## 6. Instalacja obrazu (bez modelu)
Pobiera wyłącznie obraz Docker — **nie startuje kontenera**, **nie pobiera modeli LLM**.
```bash
./scripts/pull.sh
```
Alternatywa ręczna:
```bash
docker compose --profile localai pull
```
Weryfikacja:
```bash
docker images | grep localai
docker compose --profile localai ps
```
Oczekiwane: obraz widoczny, kontener **nie** działa.
---
## 7. Start stacku
```bash
./scripts/start.sh
```
Skrypt sprawdza: `/data` zamontowany, Docker działa, tworzy katalogi w `/data/apps/localai/`.
Logi:
```bash
docker compose --profile localai logs -f localai
```
---
## 8. Weryfikacja UI i API
### 8.1 Health check
```bash
curl -s http://localhost:8070/readyz
```
Oczekiwane: odpowiedź HTTP 200 (tekst potwierdzający gotowość).
### 8.2 UI w przeglądarce
Otwórz na swoim PC (z sieci LAN):
```
http://127.0.0.1:8070
```
Adres IP serwera:
```bash
hostname -I | awk '{print $1}'
```
UI powinno się załadować — lista modeli będzie **pusta** (to OK na tym etapie).
### 8.3 API (opcjonalnie)
```bash
curl -s http://localhost:8070/v1/models -H "Authorization: Bearer $LOCALAI_API_KEY" | jq .
```
Pusta lista modeli — normalne bez pobranego modelu.
---
## 9. Zarządzanie stackiem
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/localai
docker compose --profile localai ps
docker compose --profile localai logs -f localai
docker compose --profile localai restart localai
docker compose --profile localai down
```
Przed testem modelu w LocalAI — zatrzymaj vLLM jeśli działa:
```bash
cd ../vllm
docker compose --profile vllm down
```
---
## 10. Troubleshooting
### 10.1 `/data` is not mounted
Wróć do [04-vllm-stack.md — część A](04-vllm-stack.md) i dokończ setup dysku.
### 10.2 GPU niewidoczne w kontenerze
```bash
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
```
Upewnij się, że używasz obrazu `-cuda-13`, nie `-cuda-12`.
### 10.3 Healthcheck failing / restarting
```bash
docker compose --profile localai logs --tail 100 localai
```
Pierwszy start może trwać 12 minuty. Healthcheck ma `start_period: 2m`.
### 10.4 UI niedostępne z innego komputera
LocalAI nasłuchuje na `127.0.0.1:8070`. Dostęp z LAN/internetu — przez NPMPlus (rozdział 07) lub SSH tunnel:
```bash
# na swoim PC:
ssh -L 8070:127.0.0.1:8070 tomasz-syn-grzegorza@<IP-serwera>
# potem: http://localhost:8070
```
### 10.5 Brak modeli w UI
Zamierzone na tym kroku. Modele dodasz w kolejnym etapie (Model Gallery, GGUF, CLI).
---
## 11. KV cache (po dodaniu modelu chat)
Domyślnie KV cache w llama.cpp jest w **f16** — zajmuje dużo VRAM przy długim kontekście. Na RTX 3090 Ti (24 GB) rekomendujemy **q8_0** dla K i V.
Ustawienia są w YAML modelu na `/data/apps/localai/models/<nazwa>.yaml`, sekcja `parameters:`:
| Pole | Wartość startowa |
|------|------------------|
| `cache_type_k` | `q8_0` |
| `cache_type_v` | `q8_0` |
| `flash_attention` | `true` |
| `context_size` | `8192` |
Zastosowanie z repo:
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/localai
./scripts/apply-kv-profile.sh gemma-4-12b-it-qat-q4_0
docker compose --profile localai restart localai
```
Skrypt tworzy backup YAML. Pliki w `/data/apps/localai/models/` są często **root-owned** — skrypt używa wtedy `docker exec localai` (volume `/models`).
Szczegóły: [`stacks/localai/coding-agent/KV-CACHE.md`](../stacks/localai/coding-agent/KV-CACHE.md)
Po załadowaniu modelu sprawdź VRAM:
```bash
nvidia-smi
docker compose --profile localai logs localai 2>&1 | tail -50
```
---
## 12. Następny krok
Po przejściu weryfikacji:
1. Zgłoś w rozmowie z Cursorem, że krok 05 jest gotowy.
2. Pobierz model (np. GGUF Qwen z katalogu vLLM) — osobny krok.
3. Później: rozdział **06 — sterowanie wentylatorami GPU** lub **07 — ComfyUI stack**.
---
## Checklist
- [ ] `.env` utworzony z `.env.example`
- [ ] `docker compose --profile localai pull` — obraz pobrany
- [ ] `./scripts/start.sh` — kontener `localai` running
- [ ] `curl http://localhost:8070/readyz` — OK
- [ ] UI otwiera się w przeglądarce na `:8070` (lub przez tunel SSH)
- [ ] Katalogi istnieją pod `/data/apps/localai/`
- [ ] (Po modelu chat) KV cache q8_0 w YAML + restart
+356
View File
@@ -0,0 +1,356 @@
# 06 — Sterowanie wentylatorami GPU
> **Cel rozdziału:** skonfigurować sterowanie wentylatorami RTX 3090 Ti na headless serwerze — krzywa temp → prędkość, web UI w sieci lokalnej, usługa systemd.
**Szacowany czas:** 1520 minut
**Wymagania:** ukończony rozdział [02 — Sterowniki NVIDIA](02-nvidia-driver.md) (`nvidia-smi` działa)
---
## Spis treści
1. [Dlaczego własny stack](#1-dlaczego-własny-stack)
2. [Weryfikacja punktu startowego](#2-weryfikacja-punktu-startowego)
3. [Instalacja](#3-instalacja)
4. [Web UI w sieci lokalnej](#4-web-ui-w-sieci-lokalnej)
5. [Krzywa max cooling](#5-krzywa-max-cooling)
6. [Zarządzanie usługą](#6-zarządzanie-usługą)
7. [Weryfikacja pod obciążeniem](#7-weryfikacja-pod-obciążeniem)
8. [Troubleshooting](#8-troubleshooting)
9. [Następny krok](#9-następny-krok)
---
## 1. Dlaczego własny stack
Na headless Ubuntu **nie działają** narzędzia oparte na `nvidia-settings` + CoolBits (wymagają display servera).
Od sterownika NVIDIA **520+** sterowanie wentylatorami GeForce jest dostępne przez **NVML** — działa bez monitora i bez X11.
Stack [`stacks/gpu-fan/`](../stacks/gpu-fan/):
- daemon NVML (Python + `pynvml`)
- web UI do edycji krzywej
- preset agresywny pod długie obciążenie LocalAI / vLLM
`nvidia-smi` pokazuje tylko odczyty — **nie ustawia** prędkości wentylatorów.
---
## 2. Weryfikacja punktu startowego
```bash
nvidia-smi --query-gpu=name,driver_version,temperature.gpu,fan.speed --format=csv,noheader
# Oczekiwane:
# NVIDIA GeForce RTX 3090 Ti, 595.x, <temp>, <fan> %
```
```bash
python3 -c "import pynvml; pynvml.nvmlInit(); h=pynvml.nvmlDeviceGetHandleByIndex(0); print(pynvml.nvmlDeviceGetName(h)); pynvml.nvmlShutdown()"
```
Jeśli `ModuleNotFoundError: pynvml``install.sh` doinstaluje zależności. Możesz też:
```bash
sudo apt install -y python3-pynvml
```
---
## 3. Instalacja
Z katalogu repozytorium na serwerze:
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/gpu-fan
cp .env.example .env
# opcjonalnie: edytuj GPU_FAN_PORT, API_KEY
sudo scripts/install.sh
sudo scripts/enable-lan.sh
sudo systemctl status gpu-fan
```
**Co robi `install.sh`:**
| Krok | Efekt |
|------|-------|
| `apt install python3-venv` | Środowisko Python |
| Kopiuje pliki do `/opt/gpu-fan` | Kod aplikacji |
| Tworzy `/etc/gpu-fan/curve.json` | Domyślna krzywa max cooling |
| `systemctl enable gpu-fan` | Start przy bootcie |
Logi:
```bash
journalctl -u gpu-fan -f
```
---
## 4. Web UI w sieci lokalnej
Domyślnie UI nasłuchuje na **0.0.0.0:8090** — dostępne z każdego urządzenia w LAN.
```bash
# IP serwera
hostname -I | awk '{print $1}'
# API key
grep ^API_KEY= /opt/control-plane/.env
```
W przeglądarce na laptopie / telefonie w tej samej sieci:
```
http://192.168.100.5:8090/?api_key=TWÓJ_KLUCZ_Z_.env
```
Klucz można też podać w promptcie UI przy pierwszym wejściu (bez `?api_key=` w URL).
### Tylko localhost (opcjonalnie)
W `/opt/control-plane/.env`:
```bash
GPU_FAN_HOST=127.0.0.1
API_KEY=
```
```bash
sudo systemctl restart gpu-fan
```
Dostęp przez SSH tunnel:
```bash
ssh -L 8090:127.0.0.1:8090 tomasz-syn-grzegorza@gmktec-k11
```
Otwórz: **http://localhost:8090**
### Funkcje UI
- Wykres krzywej — przeciąganie punktów; podziałka temperatury (°C) i prędkości (%), siatka pomocnicza i linijki z kreskami na zewnętrznych krawędziach osi
- Tabela temp / speed
- Status live: temperatura, wentylatory, moc, wykorzystanie GPU
- **Zapisz krzywą** — zapis do `/etc/gpu-fan/curve.json`
- **Tryb auto** — oddaje sterowanie driverowi NVIDIA
- **Manual 100%** — awaryjne pełne obroty
### API key
Przy dostępie z LAN **API_KEY jest wymagany** (generowany przy `install.sh` lub ustaw ręcznie w `.env`):
```bash
sudo nano /opt/control-plane/.env
```
```bash
sudo systemctl restart gpu-fan
```
---
## 5. Krzywa max cooling
Domyślny plik `/etc/gpu-fan/curve.json`:
```json
{
"30": 50,
"40": 65,
"50": 80,
"55": 90,
"60": 100,
"70": 100
}
```
Edycja ręczna:
```bash
sudo nano /etc/gpu-fan/curve.json
sudo systemctl reload gpu-fan
```
### Ograniczenia API NVIDIA
| Wartość | Znaczenie |
|---------|-----------|
| 0% | Oddaj sterowanie driverowi (auto) |
| 129% | **Niedozwolone** przez API |
| 30100% | Dozwolone w trybie manual |
RTX 3090 Ti w trybie **auto** może mieć **0 RPM** przy niskiej temperaturze — to normalne zachowanie karty.
---
## 6. Zarządzanie usługą
```bash
sudo systemctl start gpu-fan # start
sudo systemctl stop gpu-fan # stop + przywrócenie auto
sudo systemctl restart gpu-fan # restart
sudo systemctl reload gpu-fan # przeładuj curve.json (SIGHUP)
```
Test API z serwera:
```bash
curl -s http://127.0.0.1:8090/api/status | python3 -m json.tool
```
```bash
curl -s -X POST http://127.0.0.1:8090/api/mode \
-H 'Content-Type: application/json' \
-d '{"mode":"manual","speed":100}'
```
```bash
curl -s -X POST http://127.0.0.1:8090/api/mode \
-H 'Content-Type: application/json' \
-d '{"mode":"auto"}'
```
---
## 7. Weryfikacja pod obciążeniem
### 7.1 Idle
```bash
watch -n2 'curl -s http://127.0.0.1:8090/api/status | python3 -m json.tool'
```
W trybie `curve` przy ~40°C oczekuj target_speed zgodnego z krzywą (np. ~5065%).
### 7.2 Obciążenie GPU
Uruchom inference (LocalAI lub vLLM), np.:
```bash
# LocalAI (jeśli działa)
curl -s http://localhost:8070/v1/models
# lub krótki stress (opcjonalnie, wymaga cuda w kontenerze)
# docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi dmon -s puc -d 1 -c 30
```
Obserwuj w UI lub przez API:
- `temperature_c` rośnie pod loadem
- `target_speed_pct` podąża za krzywą
- `fan_speeds_pct` dążą do `target_speed_pct`
### 7.3 Graceful shutdown
```bash
sudo systemctl stop gpu-fan
nvidia-smi --query-gpu=fan.speed --format=csv,noheader
```
Po stopie usługi wentylatory powinny wrócić do polityki drivera (auto).
### Checklist
- [ ] `systemctl status gpu-fan` — active (running)
- [ ] Web UI dostępne z LAN (`http://<ip>:8090`)
- [ ] Zapis krzywej w UI działa
- [ ] Pod loadem temp rośnie, wentylatory przyspieszają
- [ ] Po `stop` — tryb auto drivera
---
## 8. Troubleshooting
### 8.1 `Insufficient Permissions`
Sterowanie wentylatorami wymaga **root**. Uruchamiaj przez systemd lub `sudo scripts/start.sh`.
### 8.2 `Failed to initialize NVML: Driver/library version mismatch`
Po aktualizacji kernela lub sterownika:
```bash
sudo reboot
```
### 8.3 Wentylatory nie schodzą poniżej 30%
To ograniczenie API NVIDIA w trybie manual — nie błąd aplikacji. Dla ciszy przy idle użyj **Tryb auto** w UI.
### 8.4 Port 8090 zajęty / `address already in use`
**Nie zmieniaj portu na 8091, 8092…** — to tylko maskuje problem. Przy kolejnym podwójnym starcie następny port też będzie zajęty.
Diagnostyka:
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/gpu-fan
scripts/status.sh
sudo ss -tlnp | grep -E ':809[0-9]'
jobs -l # zawieszone Ctrl+Z w tej sesji SSH
```
Czyszczenie:
```bash
sudo scripts/status.sh --cleanup
sudo systemctl start gpu-fan
```
Typowe przyczyny:
| Objaw | Przyczyna | Rozwiązanie |
|-------|-----------|-------------|
| Błąd po `systemctl restart` + `start.sh` | Dwie instancje | Używaj **albo** systemd **albo** `start.sh` |
| `[n]+ Stopped start.sh` w terminalu | Ctrl+Z zamiast Ctrl+C | `kill %n` lub `sudo scripts/status.sh --cleanup` |
| Port zajęty po reboot | `gpu-fan.service` włączone (`enable`) | Normalne — nie uruchamiaj drugiej kopii |
| Zmiana portu w repo `.env` bez efektu | Produkcja czyta `/opt/control-plane/.env` | Edytuj `/opt/control-plane/.env` lub uruchom `setup-control-plane-env.sh` |
Po zmianach w kodzie UI (np. monitoring GPU):
```bash
sudo scripts/install.sh && sudo systemctl restart gpu-fan
```
Foreground debug (tylko gdy systemd zatrzymany):
```bash
sudo systemctl stop gpu-fan
sudo scripts/start.sh
# Zakończ: Ctrl+C (nie Ctrl+Z!)
```
### 8.5 Krzywa odrzucona (400)
- 37 punktów
- temperatury rosnąco, unikalne
- prędkość: 0 lub 30100
---
## 9. Następny krok
Po przejściu checklisty:
1. Zostaw `gpu-fan` włączony przy workloadach AI (LocalAI, ComfyUI, vLLM).
2. Rozdział **07 — ComfyUI stack** — generowanie obrazów w Dockerze.
3. Rozważ `API_KEY` przed ewentualnym wystawieniem portu w rozdziale firewall (08).
Powrót do roadmapy: [README.md](../README.md)
---
## Podsumowanie
Po ukończeniu rozdziału:
- Działa `gpu-fan.service` na hoście (NVML, root)
- Krzywa w `/etc/gpu-fan/curve.json`
- Web UI na **0.0.0.0:8090** (dostęp z sieci lokalnej, z API key)
- Stop usługi przywraca auto drivera NVIDIA
+286
View File
@@ -0,0 +1,286 @@
# 07 — ComfyUI stack
> **Cel rozdziału:** uruchomić [ComfyUI](https://github.com/comfyanonymous/ComfyUI) w Dockerze z GPU, web UI na porcie **8188** i pustym katalogiem modeli. Modele dodasz później (checkpoint, LoRA, ComfyUI-Manager).
**Szacowany czas:**
- Pobranie obrazu Docker: 1030 minut (pierwszy raz)
- Pierwszy start (kopia ComfyUI do `/data`): 310 minut
**Wymagania:** ukończone rozdziały [01](01-system-update-and-docker.md)[05](05-localai-stack.md), mount `/data` z [04](04-vllm-stack.md)
**Kontekst:** Zamiast [Stability Matrix](https://github.com/LykosAI/StabilityMatrix) (GUI desktop) używamy ComfyUI w kontenerze — zgodnie z [03b](03b-system-tools.md) i researchiem w [`coding-agent/STABILITYMATRIX-RESEARCH.md`](../coding-agent/STABILITYMATRIX-RESEARCH.md).
---
## Spis treści
1. [ComfyUI vs Stability Matrix](#1-comfyui-vs-stability-matrix)
2. [Porty i architektura](#2-porty-i-architektura)
3. [Polityka GPU (LocalAI ↔ ComfyUI)](#3-polityka-gpu-localai--comfyui)
4. [Struktura plików stacku](#4-struktura-plików-stacku)
5. [Przygotowanie `.env`](#5-przygotowanie-env)
6. [Instalacja obrazu](#6-instalacja-obrazu)
7. [Start stacku](#7-start-stacku)
8. [Weryfikacja UI](#8-weryfikacja-ui)
9. [Zarządzanie stackiem](#9-zarządzanie-stackiem)
10. [Modele (później)](#10-modele-później)
11. [Troubleshooting](#11-troubleshooting)
12. [Następny krok](#12-następny-krok)
---
## 1. ComfyUI vs Stability Matrix
| | Stability Matrix | ComfyUI (`stacks/comfyui/`) |
|--|------------------|----------------------------|
| Typ | GUI desktop (AppImage) | **Docker, headless** |
| Wymaga pulpitu | Tak (X11/Wayland) | Nie — tylko przeglądarka |
| ComfyUI | Jeden z pakietów SM | **Bezpośrednio** w kontenerze |
| Zgodność z repo | Słaba na SSH-only | **Tak** |
SwarmUI (poprzedni UI obrazów) zostało usunięte — patrz [`SWARMUI-REMOVAL.md`](../coding-agent/SWARMUI-REMOVAL.md).
---
## 2. Porty i architektura
```mermaid
flowchart LR
browser["Przeglądarka :8188"]
comfyui["Kontener comfyui"]
gpu["RTX 3090 Ti"]
disk["/data/apps/comfyui/models"]
browser --> comfyui
comfyui --> gpu
comfyui --> disk
```
| Element | Wartość |
|---------|---------|
| Obraz | `yanwk/comfyui-boot:cu126-slim` |
| Web UI | `http://127.0.0.1:8188` lub `http://192.168.100.90:8188` (LAN) |
| Modele | `/data/apps/comfyui/models` |
| Docker data | `/data/docker` |
---
## 3. Polityka GPU (LocalAI ↔ ComfyUI)
RTX 3090 Ti 24 GB — **nie uruchamiaj** dużego modelu LLM (LocalAI) i dużego checkpointu SD/Flux **równocześnie**.
Przed startem ComfyUI z generowaniem obrazów:
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/localai
docker compose --profile localai stop localai
```
W Server UI (port 8091): Stop/Start stack `localai`, Start stack `comfyui`.
Skrypt `start.sh` ostrzega, gdy `localai` jest uruchomiony.
---
## 4. Struktura plików stacku
```
stacks/comfyui/
├── README.md
├── docker-compose.yml
├── .env.example
├── .gitignore
└── scripts/
├── ensure-dirs.sh
├── pull.sh
└── start.sh
```
Katalogi na dysku 1 TB:
```
/data/apps/comfyui/
├── storage/ # kopia ComfyUI (pierwszy start)
├── models/
├── cache/hf-hub/
├── cache/torch-hub/
├── input/
├── output/
├── custom_nodes/
└── workflows/
```
---
## 5. Przygotowanie `.env`
```bash
cd /home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/comfyui
cp .env.example .env
cat .env
```
Domyślne wartości:
| Zmienna | Wartość |
|---------|---------|
| `DATA_ROOT` | `/data` |
| `COMFYUI_PORT` | `8188` |
| `COMFYUI_IMAGE` | `yanwk/comfyui-boot:cu126-slim` |
| `CUDA_VISIBLE_DEVICES` | `0` |
---
## 6. Instalacja obrazu
```bash
./scripts/pull.sh
```
Oczekiwany wynik: pobranie warstw `yanwk/comfyui-boot:cu126-slim` (kilka GB).
---
## 7. Start stacku
```bash
./scripts/start.sh
```
Skrypt:
1. Sprawdza mount `/data`
2. Tworzy katalogi (`ensure-dirs.sh`)
3. Ostrzega, jeśli `localai` działa
4. `docker compose --profile comfyui up -d`
Pierwszy start kopiuje ComfyUI do `/data/apps/comfyui/storage/` — obserwuj logi:
```bash
docker compose --profile comfyui logs -f comfyui
```
---
## 8. Weryfikacja UI
```bash
docker compose --profile comfyui ps
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8188/
```
Oczekiwane: kontener `healthy` lub `running`, HTTP **200** (może zająć ~3 min przy pierwszym starcie).
W przeglądarce (z sieci lokalnej):
```
http://192.168.100.90:8188
```
Tunel SSH z laptopa:
```bash
ssh -L 8188:127.0.0.1:8188 tomasz-syn-grzegorza@gmktec-k11
# potem http://localhost:8188
```
GPU:
```bash
nvidia-smi
```
---
## 9. Zarządzanie stackiem
```bash
docker compose --profile comfyui ps
docker compose --profile comfyui logs -f comfyui
docker compose --profile comfyui restart comfyui
docker compose --profile comfyui down
```
Powrót do LocalAI:
```bash
cd ../comfyui && docker compose --profile comfyui stop comfyui
cd ../localai && docker compose --profile localai start localai
```
---
## 10. Modele (później)
Compose **nie pobiera** modeli automatycznie.
Opcje:
1. **ComfyUI-Manager** w UI — instalacja węzłów i modeli
2. Ręcznie — pliki `.safetensors` / `.ckpt` do `/data/apps/comfyui/models/checkpoints/`
Szacunki VRAM (przy zatrzymanym LocalAI):
| Model | VRAM (orientacyjnie) |
|-------|----------------------|
| SD 1.5 | ~46 GB |
| SDXL | ~812 GB |
| Flux | ~1220 GB |
---
## 11. Troubleshooting
### HTTP nie odpowiada / 000
Pierwszy start trwa dłużej — sprawdź logi:
```bash
docker compose --profile comfyui logs --tail 80 comfyui
```
### `CUDA out of memory`
Zatrzymaj LocalAI i inne workloady GPU:
```bash
docker ps --format '{{.Names}}'
nvidia-smi
```
### Brak `/data`
```bash
mountpoint /data
# jeśli nie — rozdział 04, część A
```
### Kontener restartuje się w pętli
Sprawdź, czy NVIDIA Container Toolkit działa (rozdział 03):
```bash
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi
```
---
## 12. Następny krok
Po przejściu weryfikacji:
1. Zgłoś w rozmowie z Cursorem, że krok 07 jest gotowy.
2. Pobierz pierwszy checkpoint (SD 1.5 lub SDXL) — osobny krok.
3. Później: rozdział **08 — firewall i hardening**.
---
## Checklist
- [ ] `.env` utworzony z `.env.example`
- [ ] `docker compose --profile comfyui pull` — obraz pobrany
- [ ] LocalAI zatrzymany przed pierwszym testem generowania (opcjonalnie przy samym starcie UI)
- [ ] `./scripts/start.sh` — kontener `comfyui` running
- [ ] `curl http://127.0.0.1:8188/` — HTTP 200
- [ ] UI otwiera się w przeglądarce na `:8188`
- [ ] Katalogi istnieją pod `/data/apps/comfyui/`
+230
View File
@@ -0,0 +1,230 @@
# 08 — Server UI i panel sterowania (Control Plane)
> **Cel rozdziału:** zainstalować panel Server UI do zarządzania stackami Docker (LocalAI, ComfyUI, vLLM, NPMPlus) oraz zakładkę GPU Fan — i wiedzieć, gdzie są klucze API.
**Szacowany czas:** 1015 minut
**Wymagania:** rozdział [01 — Docker](01-system-update-and-docker.md); opcjonalnie [06 — gpu-fan](06-gpu-fan-control.md) dla zakładki wentylatorów
---
## Spis treści
1. [Co to jest Server UI](#1-co-to-jest-server-ui)
2. [Dlaczego nie ma go w docker ps](#2-dlaczego-nie-ma-go-w-docker-ps)
3. [Instalacja jednym skryptem](#3-instalacja-jednym-skryptem)
4. [Gdzie są klucze API](#4-gdzie-są-klucze-api)
5. [Jak uruchomić i otworzyć panel](#5-jak-uruchomić-i-otworzyć-panel)
6. [Tryb Docker (opcjonalnie)](#6-tryb-docker-opcjonalnie)
7. [Troubleshooting](#7-troubleshooting)
8. [Następny krok](#8-następny-krok)
---
## 1. Co to jest Server UI
**Server UI** to jeden panel w przeglądarce (port **8091**), w którym możesz:
- włączać i wyłączać stacki Docker (Start / Stop / Restart),
- przeglądać logi kontenerów,
- otwierać linki do LocalAI, ComfyUI, vLLM, NPMPlus,
- zmieniać porty usług (zapis do `.env` stacku),
- sterować wentylatorami GPU (zakładka **GPU Fan**).
Zastępuje Portainer i Dockge.
---
## 2. Dlaczego nie ma go w `docker ps`
Komenda `docker ps` pokazuje **tylko kontenery Docker**.
| Co widzisz w `docker ps` | Co to jest |
|--------------------------|------------|
| `comfyui`, `npmplus`, `localai` | Stacki AI / proxy — **Docker** |
| **Brak** `server-ui` | Panel działa jako **usługa systemd** na hoście (domyślnie) |
| **Brak** `gpu-fan` | Agent wentylatorów — też **systemd** na hoście |
To **nie jest błąd**. Panel nie musi być kontenerem.
Sprawdzenie:
```bash
systemctl status server-ui gpu-fan
ss -tln | grep -E '8091|18090'
```
Jeśli wybrałeś instalację Server UI w **Dockerze** (opcja 2 w instalatorze), wtedy zobaczysz kontener:
```bash
docker compose --profile server-ui ps
```
---
## 3. Instalacja jednym skryptem
Na serwerze:
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/server-ui
sudo ./scripts/install-control-plane.sh
```
### Menu instalatora
**1) gpu-fan** — agent sterowania wentylatorami GPU
- Działa **tylko na hoście** (root + NVML).
- **Nie instalujemy go w Dockerze** — karta wymaga bezpośredniego dostępu do sterownika NVIDIA.
- Zalecane: **Y** (tak).
**2) Server UI** — panel w przeglądarce
| Opcja | Opis |
|-------|------|
| **1 — Native** | systemd, `/opt/server-ui` (zalecane) |
| **2 — Docker** | kontener z dostępem do `docker.sock` |
| **3 — Pomiń** | tylko gpu-fan |
### Instalacja bez pytań (domyślnie: gpu-fan + server-ui native)
```bash
sudo ./scripts/install-control-plane.sh -y
```
### Tylko Server UI (bez gpu-fan)
```bash
sudo ./scripts/install-control-plane.sh --gpu-fan=no --server-ui=native
```
---
## 4. Klucz API — wpisz w panelu
**Pełna instrukcja krok po kroku:** [04a — Klucz API](04a-api-key.md)
Skrót po instalacji:
1. Na serwerze: `sudo grep ^API_KEY= /opt/control-plane/.env` — skopiuj wartość po `=`
2. W przeglądarce: `http://<IP-serwera>:8091/` → pole **API Key****Zapisz****Sprawdź klucz**
3. Lub gotowy link: `bash stacks/server-ui/scripts/show-api-key.sh` (wypisze URL z kluczem)
Jeden plik, jeden klucz — `/opt/control-plane/.env` (Server UI + gpu-fan).
**Nie używaj** `stacks/server-ui/.env` ani `GPU_FAN_AGENT_KEY` — to przestarzałe.
---
## 5. Jak uruchomić i otworzyć panel
### Native (systemd)
```bash
sudo systemctl status server-ui
sudo systemctl start server-ui # jeśli zatrzymany
sudo systemctl restart server-ui # po aktualizacji kodu
```
Adres (przykład):
```
http://192.168.100.90:8091/
```
### gpu-fan (jeśli zainstalowany)
```bash
sudo systemctl status gpu-fan
journalctl -u gpu-fan -f
```
### Weryfikacja API
```bash
curl -s http://127.0.0.1:8091/api/health
curl -s http://127.0.0.1:8091/api/stacks
```
### Aktualizacja kodu po zmianach w repo
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/server-ui
sudo ./scripts/install.sh && sudo systemctl restart server-ui
```
---
## 6. Tryb Docker (opcjonalnie)
Jeśli w instalatorze wybrałeś **2 — Docker**:
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/server-ui
docker compose --profile server-ui ps
docker compose --profile server-ui logs -f
docker compose --profile server-ui down # stop
sudo ./scripts/install-docker.sh # ponowna instalacja
```
gpu-fan **nadal** działa na hoście (systemd) — kontener Server UI łączy się z agentem przez `host.docker.internal:18090`.
Przełączenie z Docker na native:
```bash
docker compose --profile server-ui down
sudo ./scripts/install.sh
```
---
## 7. Troubleshooting
### Port 8091 zajęty
```bash
ss -tlnp | grep 8091
systemctl status server-ui
```
Zobacz też: [06 — gpu-fan, sekcja portów](06-gpu-fan-control.md#84-port-8090-zajęty--address-already-in-use) (podobna sytuacja — nie zwiększaj portu bez diagnozy).
### Zakładka GPU Fan nie działa
1. Czy gpu-fan działa: `systemctl status gpu-fan`
2. Czy agent odpowiada:
```bash
API_KEY=$(grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)
curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: $API_KEY"
```
3. Ten sam klucz w panelu Server UI (pole API Key lub `?api_key=`)
### Start stacku zwraca 401 / Invalid API key
Zobacz [04a — Klucz API, sekcja 7](04a-api-key.md#7-błąd-invalid-or-missing-api-key--checklist).
### Start stacku zwraca 409
Tylko **jeden** duży workload GPU naraz (LocalAI **lub** ComfyUI **lub** vLLM). Zatrzymaj działający stack GPU przed startem drugiego.
---
## 8. Następny krok
- Zarządzaj stackami z panelu :8091
- Edytuj porty usług w kartach stacków (pole **Zapisz port**)
- Przy zmianie portu LocalAI zaktualizuj upstream w NPMPlus ręcznie
Powrót do roadmapy: [README.md](../README.md)
---
## Podsumowanie
| Pytanie | Odpowiedź |
|---------|-----------|
| Gdzie panel? | `http://<IP>:8091/` |
| Dlaczego nie w `docker ps`? | Domyślnie systemd, nie kontener |
| Klucz (Server UI + gpu-fan) | [04a — Klucz API](04a-api-key.md) · `/opt/control-plane/.env` |
| Instalacja | `sudo ./scripts/install-control-plane.sh` |
+117
View File
@@ -0,0 +1,117 @@
# 09 — Przeglądarka plików w Server UI
> **Cel rozdziału:** przeglądać i edytować pliki na serwerze z przeglądarki (zakładka **Pliki** w panelu :8091).
**Szacowany czas:** 10 minut
**Wymagania:** rozdział [08 — Server UI](08-server-ui-install.md) (działający panel + klucz API)
---
## 1. Otwórz zakładkę Pliki
1. Wejdź na panel: `http://<IP-serwera>:8091/`
2. Wpisz **API Key** (toolbar u góry) i kliknij **Zapisz**
3. Kliknij zakładkę **Pliki** (druga od lewej)
Bezpośredni link:
```
http://<IP-serwera>:8091/#files?api_key=TWÓJ_KLUCZ
```
---
## 2. Klucz API
Zobacz [04a — Klucz API](04a-api-key.md): pole **API Key****Zapisz****Sprawdź klucz**.
---
## 3. Nawigacja
| Element | Działanie |
|---------|-----------|
| Lista po lewej | Klik na **folder** → wejście; klik na **plik** → otwarcie w edytorze |
| Breadcrumb u góry | Skróty do katalogów nadrzędnych |
| **↑ Wyżej** | Katalog rodzica |
| **Odśwież** | Ponowne wczytanie listy |
Przykładowe ścieżki:
| Co chcesz zrobić | Ścieżka |
|------------------|---------|
| Repo projektu | `/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal` |
| `.env` stacku LocalAI | `.../stacks/localai/.env` |
| Dane aplikacji | `/data/apps/` |
| Logi systemowe | `/var/log/` (jeśli masz uprawnienia) |
---
## 4. Edycja pliku
1. Kliknij plik tekstowy na liście
2. Zawartość pojawi się po prawej
3. Edytuj w polu tekstowym
4. Kliknij **Zapisz**
Pliki **binarne** (obrazy, modele) — tylko podgląd base64, bez zapisu.
---
## 5. Tworzenie i usuwanie
| Przycisk | Działanie |
|----------|-----------|
| **+ Folder** | Nowy podfolder w bieżącym katalogu |
| **+ Plik** | Nowy pusty plik |
| **Zmień nazwę** | Zaznacz element na liście, potem przycisk |
| **Usuń** | Zaznacz element — pojawi się potwierdzenie |
**Uwaga:** Nie można usunąć niepustego folderu — najpierw opróżnij go.
---
## 6. Brak uprawnień — co to znaczy?
Panel działa jako Twój użytkownik Linux (`tomasz-syn-grzegorza`), **nie jako root**.
| Objaw | Przyczyna | Rozwiązanie |
|-------|-----------|-------------|
| „Brak uprawnień” przy `/opt/control-plane/.env` | Plik root-only (600) | `sudo nano /opt/control-plane/.env` w SSH |
| Nie widać cudzego katalogu | Brak prawa odczytu | `sudo` w terminalu lub zmiana właściciela |
| Zapis odrzucony | Brak prawa zapisu | `chmod` / `chown` w SSH |
To normalne — explorer nie omija zabezpieczeń systemu.
---
## 7. Weryfikacja
1. Wejdź w `/tmp`
2. Kliknij **+ Plik**, nazwa: `server-ui-test.txt`
3. Wpisz tekst, **Zapisz**
4. Odśwież — plik na liście
5. **Usuń** plik
---
## 8. Następny krok
- Terminal w przeglądarce: [10 — CLI](10-server-ui-cli.md)
- Edytuj `.env` stacków w `stacks/<nazwa>/.env`
- Zarządzaj stackami w zakładce **Stacki**
- Steruj wentylatorami w **GPU Fan**
Powrót: [README.md](../README.md)
---
## Podsumowanie
| Pytanie | Odpowiedź |
|---------|-----------|
| Gdzie panel? | `http://<IP>:8091/#files` |
| Klucz API? | `sudo grep ^API_KEY= /opt/control-plane/.env` |
| Limit rozmiaru pliku? | 2 MiB (domyślnie) |
| Root w przeglądarce? | Nie — tylko uprawnienia użytkownika panelu |
+122
View File
@@ -0,0 +1,122 @@
# 10 — Terminal CLI w Server UI
> **Cel rozdziału:** uruchomić shell bash w przeglądarce (zakładka **CLI** w panelu :8091) — jak krótkie SSH, ale w UI.
**Szacowany czas:** 5 minut
**Wymagania:** rozdział [08 — Server UI](08-server-ui-install.md) (działający panel + klucz API)
---
## 1. Otwórz zakładkę CLI
1. Wejdź na panel: `http://<IP-serwera>:8091/`
2. Wpisz **API Key** (toolbar u góry) i kliknij **Zapisz**
3. Kliknij zakładkę **CLI** (pierwsza od lewej)
Bezpośredni link:
```
http://<IP-serwera>:8091/#cli?api_key=TWÓJ_KLUCZ
```
Status u góry terminala:
- **Połączono** (zielony) — gotowe
- **Wpisz i zapisz API Key** — brak klucza
- **Rozłączono** — kliknij **Połącz ponownie**
---
## 2. Klucz API
Zobacz [04a — Klucz API](04a-api-key.md): pole **API Key****Zapisz****Sprawdź klucz**.
---
## 3. Podstawowe komendy
W terminalu wpisz jak w SSH:
```bash
whoami
pwd
ls
docker ps
```
Powinieneś zobaczyć użytkownika `tomasz-syn-grzegorza` i dostęp do Dockera.
---
## 4. Programy interaktywne
| Program | Jak wyjść |
|---------|-----------|
| `htop` | klawisz `q` |
| `less plik` | `q` |
| `vim plik` | `:q` lub `:wq` |
| `nano plik` | `Ctrl+O`, `Ctrl+X` |
Zmiana rozmiaru okna przeglądarki dostosowuje terminal automatycznie.
---
## 5. sudo
Możesz użyć `sudo` — terminal poprosi o hasło **Twojego użytkownika Linux** (nie API Key):
```bash
sudo ls /root
```
---
## 6. Ograniczenia
| Co | Wyjaśnienie |
|----|-------------|
| Nie jesteś rootem | Panel działa jako Twój użytkownik — jak zwykłe SSH |
| Nowa sesja po reconnect | Zamknięcie zakładki / **Połącz ponownie** = nowy bash |
| Limit sesji | Domyślnie max 5 równoległych terminali (wszystkie karty) |
| Wyłączenie CLI | Admin może ustawić `CLI_ENABLED=0` w `/opt/control-plane/.env` |
---
## 7a. Pusty terminal (inne zakładki działają)
1. U góry: **Zapisz****Sprawdź klucz** (musi być „Klucz poprawny”)
2. Kliknij **Połącz ponownie**
3. Status powinien zmienić się na **Połączono** — pojawi się prompt bash
4. Jeśli nadal pusto: twardy refresh strony (Ctrl+F5), potem kroki 12
5. Zły klucz: status „Zły klucz API…” — zobacz [04a — Klucz API](04a-api-key.md)
---
## 7. Weryfikacja
1. Zakładka **CLI** → status **Połączono**
2. `echo test-cli-ok` → widzisz `test-cli-ok`
3. `docker ps` → lista kontenerów
4. Zmień rozmiar okna — prompt nie „łamie się” dziwnie
5. Przełącz na **Stacki** i wróć do **CLI** — nowa sesja (to normalne)
---
## 8. Następny krok
- Przeglądaj pliki w zakładce [Pliki](09-file-explorer.md)
- Zarządzaj stackami w **Stacki**
- Steruj wentylatorami w **GPU Fan**
Powrót: [README.md](../README.md)
---
## Podsumowanie
| Pytanie | Odpowiedź |
|---------|-----------|
| Gdzie terminal? | `http://<IP>:8091/#cli` |
| Klucz API? | [04a — Klucz API](04a-api-key.md) |
| Kto jesteś w shellu? | `tomasz-syn-grzegorza` (nie root) |
| vim/htop? | Tak — pełny PTY |
+421
View File
@@ -0,0 +1,421 @@
#!/usr/bin/env bash
# Statyczne IP na dwóch kartach Ethernet — GMKtec K11 (gmktec-k11)
#
# Skanuje link na eno1 / enp3s0, pokazuje menu CLI i generuje Netplan.
# Rollback: scripts/restore-dhcp-network.sh
set -euo pipefail
NETPLAN_FILE="/etc/netplan/00-installer-config.yaml"
NIC_A="eno1"
NIC_B="enp3s0"
IP80="192.168.100.80/24"
IP90="192.168.100.90/24"
GATEWAY="192.168.100.1"
DNS="192.168.100.1"
DRY_RUN=false
NON_INTERACTIVE=false
PROFILE=""
# Po wyborze profilu (ustawiane przez apply_profile)
GW_NIC=""
ENO1_IPS=""
ENP3S0_IPS=""
PROFILE_NAME=""
usage() {
cat <<'EOF'
Użycie: sudo ./configure-static-dual-nic.sh [opcje]
Opcje:
--dry-run Skan + menu (lub profil) + YAML, bez zapisu i apply
--non-interactive Pomiń menu (wymaga --profile)
--profile <nazwa> Profil konfiguracji (patrz niżej)
-h, --help Ta pomoc
Profile (--non-interactive):
dual-eno1 eno1=.80+brama, enp3s0=.90
dual-enp3s0 enp3s0=.80+brama, eno1=.90
single-eno1-80 tylko eno1: .80+brama
single-eno1-both tylko eno1: .80+.90+brama
single-enp3s0-90 tylko enp3s0: .90+brama
single-enp3s0-80 tylko enp3s0: .80+brama
single-enp3s0-both tylko enp3s0: .80+.90+brama
Interaktywnie skrypt skanuje kable i pokazuje tylko pasujące opcje.
Rollback: sudo ./restore-dhcp-network.sh
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--non-interactive) NON_INTERACTIVE=true; shift ;;
--profile)
if [[ $# -lt 2 ]]; then
echo "BŁĄD: --profile wymaga nazwy." >&2
exit 1
fi
PROFILE="$2"
shift 2
;;
-h|--help) usage; exit 0 ;;
*) echo "Nieznana opcja: $1" >&2; usage >&2; exit 1 ;;
esac
done
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "BŁĄD: Uruchom skrypt jako root (sudo)." >&2
exit 1
fi
if ! command -v netplan >/dev/null 2>&1; then
echo "BŁĄD: netplan nie jest zainstalowany." >&2
exit 1
fi
for nic in "$NIC_A" "$NIC_B"; do
if ! ip link show "$nic" &>/dev/null; then
echo "BŁĄD: Interfejs $nic nie istnieje." >&2
ip -br link || true
exit 1
fi
done
has_link() {
local nic="$1"
if command -v ethtool >/dev/null 2>&1; then
ethtool "$nic" 2>/dev/null | grep -q "Link detected: yes"
return $?
fi
ip link show "$nic" | grep -q "LOWER_UP"
}
current_ipv4() {
local nic="$1"
ip -4 -br addr show "$nic" 2>/dev/null | awk '{print $3}' | head -1
}
# Zwraca: LINK_ENO1 LINK_ENP3S0 (yes/no)
scan_nics() {
LINK_ENO1="no"
LINK_ENP3S0="no"
has_link "$NIC_A" && LINK_ENO1="yes"
has_link "$NIC_B" && LINK_ENP3S0="yes"
echo "=== Skan interfejsów ==="
printf " %-8s %-6s %-8s %s\n" "Interfejs" "Stan" "Link" "IPv4"
for nic in "$NIC_A" "$NIC_B"; do
local state link_flag ipv4
state=$(ip -br link show "$nic" | awk '{print $2}')
if has_link "$nic"; then link_flag="TAK"; else link_flag="NIE"; fi
ipv4=$(current_ipv4 "$nic")
ipv4="${ipv4:-}"
printf " %-8s %-6s %-8s %s\n" "$nic" "$state" "$link_flag" "$ipv4"
done
echo ""
}
ips_has() {
local list="$1"
local which="$2"
[[ " $list " == *" $which "* ]]
}
apply_profile() {
local key="$1"
PROFILE_NAME="$key"
case "$key" in
dual-eno1)
GW_NIC="$NIC_A"; ENO1_IPS="80"; ENP3S0_IPS="90" ;;
dual-enp3s0)
GW_NIC="$NIC_B"; ENO1_IPS="90"; ENP3S0_IPS="80" ;;
single-eno1-80)
GW_NIC="$NIC_A"; ENO1_IPS="80"; ENP3S0_IPS="" ;;
single-eno1-both)
GW_NIC="$NIC_A"; ENO1_IPS="80 90"; ENP3S0_IPS="" ;;
single-enp3s0-90)
GW_NIC="$NIC_B"; ENO1_IPS=""; ENP3S0_IPS="90" ;;
single-enp3s0-80)
GW_NIC="$NIC_B"; ENO1_IPS=""; ENP3S0_IPS="80" ;;
single-enp3s0-both)
GW_NIC="$NIC_B"; ENO1_IPS=""; ENP3S0_IPS="80 90" ;;
*)
echo "BŁĄD: Nieznany profil: $key" >&2
return 1
;;
esac
}
profile_description() {
local key="$1"
case "$key" in
dual-eno1) echo "eno1=.80+brama, enp3s0=.90" ;;
dual-enp3s0) echo "enp3s0=.80+brama, eno1=.90" ;;
single-eno1-80) echo "eno1=.80+brama (enp3s0 bez adresu)" ;;
single-eno1-both) echo "eno1=.80+.90+brama (oba IP na eno1)" ;;
single-enp3s0-90) echo "enp3s0=.90+brama (usługi LAN)" ;;
single-enp3s0-80) echo "enp3s0=.80+brama" ;;
single-enp3s0-both) echo "enp3s0=.80+.90+brama (oba IP na enp3s0)" ;;
*) echo "$key" ;;
esac
}
# MENU_KEYS[], MENU_LABELS[] — numeracja od 1
declare -a MENU_KEYS=()
declare -a MENU_LABELS=()
build_menu() {
MENU_KEYS=()
MENU_LABELS=()
if [[ "$LINK_ENO1" == "yes" && "$LINK_ENP3S0" == "yes" ]]; then
MENU_KEYS+=(dual-eno1 dual-enp3s0)
MENU_LABELS+=(
"Dual: eno1=.80+brama, enp3s0=.90 (zalecane)"
"Dual odwrócony: enp3s0=.80+brama, eno1=.90"
)
elif [[ "$LINK_ENO1" == "yes" ]]; then
MENU_KEYS+=(single-eno1-80 single-eno1-both)
MENU_LABELS+=(
"Pojedynczy eno1: .80+brama"
"Pojedynczy eno1: .80+.90+brama (awaryjnie, oba IP na jednej karcie)"
)
elif [[ "$LINK_ENP3S0" == "yes" ]]; then
MENU_KEYS+=(single-enp3s0-90 single-enp3s0-80 single-enp3s0-both)
MENU_LABELS+=(
"Pojedynczy enp3s0: .90+brama (zalecane — usługi Docker)"
"Pojedynczy enp3s0: .80+brama"
"Pojedynczy enp3s0: .80+.90+brama (oba IP na jednej karcie)"
)
else
echo "BŁĄD: Żaden interfejs nie ma aktywnego linku (kabel sieciowy)." >&2
echo " Podłącz kabel do eno1 lub enp3s0 i uruchom skrypt ponownie." >&2
exit 1
fi
}
show_menu() {
echo "=== Wybierz konfigurację ==="
local i
for i in "${!MENU_KEYS[@]}"; do
printf " %d) %s\n" "$((i + 1))" "${MENU_LABELS[$i]}"
done
printf " 0) Anuluj\n"
echo ""
}
pick_profile_interactive() {
build_menu
show_menu
local choice
while true; do
read -r -p "Wybór [1-${#MENU_KEYS[@]}, domyślnie 1]: " choice
choice="${choice:-1}"
if [[ "$choice" == "0" ]]; then
echo "Anulowano."
exit 0
fi
if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#MENU_KEYS[@]} )); then
apply_profile "${MENU_KEYS[$((choice - 1))]}"
return 0
fi
echo "Nieprawidłowy wybór."
done
}
pick_profile() {
if [[ -n "$PROFILE" ]]; then
apply_profile "$PROFILE"
elif [[ "$NON_INTERACTIVE" == true ]]; then
echo "BŁĄD: --non-interactive wymaga --profile <nazwa>." >&2
exit 1
else
pick_profile_interactive
fi
}
build_nic_block() {
local nic="$1"
local ips="$2"
local is_gw="$3"
local has_cable="$4"
local optional="true"
[[ "$is_gw" == "yes" ]] && optional="false"
echo " ${nic}:"
echo " dhcp4: false"
echo " dhcp6: false"
echo " optional: ${optional}"
if [[ -n "$ips" ]]; then
echo " ignore-carrier: true"
echo " addresses:"
ips_has "$ips" "80" && echo " - ${IP80}"
ips_has "$ips" "90" && echo " - ${IP90}"
fi
if [[ "$is_gw" == "yes" ]]; then
echo " routes:"
echo " - to: default"
echo " via: ${GATEWAY}"
echo " metric: 100"
echo " nameservers:"
echo " addresses:"
echo " - ${DNS}"
fi
}
write_netplan_file() {
local content="$1"
local tmp
tmp=$(mktemp) || { echo "BŁĄD: mktemp nie powiódł się." >&2; return 1; }
chmod 600 "$tmp"
printf '%s\n' "$content" > "$tmp"
mv -f "$tmp" "$NETPLAN_FILE"
chmod 600 "$NETPLAN_FILE"
}
render_netplan() {
local eno1_gw="no" enp3_gw="no"
[[ "$GW_NIC" == "$NIC_A" ]] && eno1_gw="yes"
[[ "$GW_NIC" == "$NIC_B" ]] && enp3_gw="yes"
{
echo "network:"
echo " version: 2"
echo " renderer: networkd"
echo " ethernets:"
build_nic_block "$NIC_A" "$ENO1_IPS" "$eno1_gw" "$LINK_ENO1"
build_nic_block "$NIC_B" "$ENP3S0_IPS" "$enp3_gw" "$LINK_ENP3S0"
}
}
expected_ips_for_nic() {
local nic="$1"
local ips=""
if [[ "$nic" == "$NIC_A" ]]; then ips="$ENO1_IPS"; else ips="$ENP3S0_IPS"; fi
if ips_has "$ips" "80"; then echo "192.168.100.80"; fi
if ips_has "$ips" "90"; then echo "192.168.100.90"; fi
}
verify_config() {
local ok=true
echo "=== Weryfikacja ==="
for nic in "$NIC_A" "$NIC_B"; do
local ip
while read -r ip; do
[[ -z "$ip" ]] && continue
if ip -4 addr show dev "$nic" | grep -q "inet ${ip}/"; then
echo " ${nic}: ${ip} OK"
else
echo " ${nic}: ${ip} BRAK — próbuję networkctl reconfigure ..."
networkctl reconfigure "$nic" 2>/dev/null || true
sleep 2
if ip -4 addr show dev "$nic" | grep -q "inet ${ip}/"; then
echo " ${nic}: ${ip} OK (po reconfigure)"
else
echo " ${nic}: ${ip} NADAL BRAK"
ok=false
fi
fi
done < <(expected_ips_for_nic "$nic")
done
ip -br addr show "$NIC_A" "$NIC_B" 2>/dev/null || ip -br addr show "$NIC_A" "$NIC_B"
echo "--- trasy ---"
ip route show default || echo " (brak trasy domyślnej)"
if ping -c 1 -W 3 -I "$GW_NIC" "$GATEWAY" &>/dev/null; then
echo " ping -I ${GW_NIC} ${GATEWAY}: OK"
else
echo " ping -I ${GW_NIC} ${GATEWAY}: NIEUDANY"
ok=false
fi
if [[ "$ok" != true ]]; then
echo ""
echo "WERYFIKACJA NIEUDANA — sprawdź kable, rezerwację IP na routerze."
echo "Rollback: sudo $(dirname "$0")/restore-dhcp-network.sh"
return 1
fi
return 0
}
wait_for_ips() {
local attempt
for attempt in $(seq 1 15); do
local missing=false
for nic in "$NIC_A" "$NIC_B"; do
local ip
while read -r ip; do
[[ -z "$ip" ]] && continue
ip -4 addr show dev "$nic" | grep -q "inet ${ip}/" || missing=true
done < <(expected_ips_for_nic "$nic")
done
[[ "$missing" == false ]] && return 0
networkctl reconfigure "$NIC_A" 2>/dev/null || true
networkctl reconfigure "$NIC_B" 2>/dev/null || true
sleep 1
done
return 1
}
# --- main ---
scan_nics
pick_profile
STATIC_YAML=$(render_netplan)
echo "=== Wybrany profil: ${PROFILE_NAME} ==="
echo " $(profile_description "$PROFILE_NAME")"
echo " Brama domyślna: ${GW_NIC}"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "=== DRY-RUN: docelowy Netplan ($NETPLAN_FILE) ==="
echo "$STATIC_YAML"
echo ""
echo "Dry-run: backup, zapis i netplan apply zostały pominięte."
exit 0
fi
echo "=== Backup Netplan ==="
if [[ -f "$NETPLAN_FILE" ]]; then
backup="${NETPLAN_FILE}.bak.$(date +%Y%m%d%H%M%S)"
cp -a "$NETPLAN_FILE" "$backup"
echo " Zapisano: $backup"
else
echo " Brak istniejącego $NETPLAN_FILE — backup pominięty."
fi
echo "=== Zapis statycznej konfiguracji ==="
write_netplan_file "$STATIC_YAML"
echo " Zapisano: $NETPLAN_FILE"
echo "=== netplan generate ==="
if ! netplan generate 2>&1; then
echo ""
echo "BŁĄD: netplan generate nie powiódł się. Przywróć backup lub uruchom restore-dhcp-network.sh"
exit 1
fi
echo "=== netplan apply ==="
netplan apply
wait_for_ips || true
verify_config || exit 1
echo ""
echo "=== GOTOWE ==="
echo " Profil: ${PROFILE_NAME}$(profile_description "$PROFILE_NAME")"
echo " Brama: ${GW_NIC} -> ${GATEWAY}"
[[ -n "$ENO1_IPS" ]] && echo " eno1: ${ENO1_IPS} -> 192.168.100.{${ENO1_IPS// /,}}"
[[ -n "$ENP3S0_IPS" ]] && echo " enp3s0: ${ENP3S0_IPS} -> 192.168.100.{${ENP3S0_IPS// /,}}"
echo ""
echo "SSH: sprawdź ip -br addr (adresy .80 / .90 na aktywnych kartach)"
echo "Rollback: sudo $(dirname "$0")/restore-dhcp-network.sh"
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
# Przywrócenie automatycznego IP (DHCP) — GMKtec K11 (gmktec-k11)
#
# Kolejność:
# 1. Przywróć najnowszy *poprawny* backup /etc/netplan/00-installer-config.yaml.bak.*
# 2. Jeśli brak poprawnego backupu — szablon DHCP na eno1 i enp3s0
#
# Wymaga: sudo
set -euo pipefail
NETPLAN_FILE="/etc/netplan/00-installer-config.yaml"
NIC_PRIMARY="eno1"
NIC_LAN="enp3s0"
DRY_RUN=false
FORCE_DHCP=false
BACKUP_FILE=""
usage() {
cat <<'EOF'
Użycie: sudo ./restore-dhcp-network.sh [opcje]
Opcje:
--dry-run Pokaż plan bez zapisu ani netplan apply
--force-dhcp Pomiń backupy — od razu szablon DHCP
--backup <ścieżka> Przywróć wskazany plik backupu (musi przejść netplan generate)
-h, --help Ta pomoc
Po rollbacku sprawdź: ip -br addr
Sesja SSH może być pod starym adresem DHCP (np. 192.168.100.2).
Ponowna konfiguracja statyczna (skan + menu):
sudo ./configure-static-dual-nic.sh
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--force-dhcp) FORCE_DHCP=true; shift ;;
--backup)
if [[ $# -lt 2 ]]; then
echo "BŁĄD: --backup wymaga ścieżki." >&2
exit 1
fi
BACKUP_FILE="$2"
shift 2
;;
-h|--help) usage; exit 0 ;;
*) echo "Nieznana opcja: $1" >&2; usage >&2; exit 1 ;;
esac
done
if [[ "${EUID:-$(id -u)}" -ne 0 ]]; then
echo "BŁĄD: Uruchom skrypt jako root (sudo)." >&2
exit 1
fi
if ! command -v netplan >/dev/null 2>&1; then
echo "BŁĄD: netplan nie jest zainstalowany." >&2
exit 1
fi
DHCP_YAML=$(cat <<EOF
network:
version: 2
ethernets:
${NIC_PRIMARY}:
dhcp4: true
${NIC_LAN}:
dhcp4: true
EOF
)
write_netplan_file() {
local content="$1"
local tmp
tmp=$(mktemp) || { echo "BŁĄD: mktemp nie powiódł się." >&2; return 1; }
chmod 600 "$tmp"
printf '%s\n' "$content" > "$tmp"
mv -f "$tmp" "$NETPLAN_FILE"
chmod 600 "$NETPLAN_FILE"
}
netplan_file_valid() {
local file="$1"
local tmpdir
[[ -f "$file" ]] || return 1
tmpdir=$(mktemp -d) || return 1
mkdir -p "$tmpdir/etc/netplan"
cp "$file" "$tmpdir/etc/netplan/00-installer-config.yaml"
if netplan generate --root-dir "$tmpdir" &>/dev/null; then
rm -rf "$tmpdir"
return 0
fi
rm -rf "$tmpdir"
return 1
}
find_valid_backup() {
local f
if [[ -n "$BACKUP_FILE" ]]; then
if netplan_file_valid "$BACKUP_FILE"; then
echo "$BACKUP_FILE"
return 0
fi
echo "BŁĄD: Backup nie przechodzi netplan generate: $BACKUP_FILE" >&2
return 1
fi
# Najpierw najnowsze poprawne; pomijaj zepsute backupy ze skryptu statycznego
while IFS= read -r f; do
[[ -z "$f" ]] && continue
if netplan_file_valid "$f"; then
echo "$f"
return 0
fi
echo " Pomijam niepoprawny backup: $f" >&2
done < <(ls -1t "${NETPLAN_FILE}.bak."* 2>/dev/null || true)
return 1
}
echo "=== Przywracanie DHCP ==="
source_file=""
action="dhcp-fallback"
if [[ "$FORCE_DHCP" != true ]]; then
if backup_path=$(find_valid_backup); then
echo " Źródło: backup $backup_path"
source_file="$backup_path"
action="restore-backup"
else
echo " Brak poprawnego backupu — używam szablonu DHCP."
fi
else
echo " --force-dhcp: szablon DHCP (bez backupów)."
fi
if [[ "$DRY_RUN" == true ]]; then
echo "=== DRY-RUN ==="
if [[ "$action" == "restore-backup" ]]; then
echo "Przywrócono by został plik z: $source_file"
echo "--- zawartość backupu ---"
cat "$source_file"
else
echo "Zapisano by został $NETPLAN_FILE:"
echo "$DHCP_YAML"
fi
echo ""
echo "Dry-run: netplan apply pominięty."
exit 0
fi
if [[ "$action" == "restore-backup" ]]; then
cp -a "$source_file" "$NETPLAN_FILE"
echo " Przywrócono: $NETPLAN_FILE <- $source_file"
else
write_netplan_file "$DHCP_YAML"
echo " Zapisano szablon DHCP: $NETPLAN_FILE"
fi
echo "=== netplan generate ==="
if ! netplan generate 2>&1; then
echo ""
echo "BŁĄD: netplan generate nie powiódł się."
if [[ "$action" == "restore-backup" ]]; then
echo "Próbuję szablon DHCP..."
write_netplan_file "$DHCP_YAML"
netplan generate
else
exit 1
fi
fi
echo "=== netplan apply ==="
netplan apply
sleep 2
echo "=== Weryfikacja ==="
ip -br addr show "$NIC_PRIMARY" "$NIC_LAN" 2>/dev/null || ip -br addr
echo ""
echo "=== GOTOWE (DHCP) ==="
echo "Sprawdź aktualne adresy: ip -br addr"
echo "Połącz się ponownie SSH pod adresem DHCP (np. ip addr show enp3s0)."
echo "Aby ponownie ustawić statyczne IP: sudo $(dirname "$0")/configure-static-dual-nic.sh"
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# Setup 1TB data disk on GMKtec K11
# nvme0n1 = 1TB data | nvme1n1 = 128GB system
set -euo pipefail
DATA_DISK=/dev/nvme0n1
DATA_PART=/dev/nvme0n1p1
MOUNT_POINT=/data
echo "=== Sprawdzam dyski ==="
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,MODEL
# Safety: refuse if DATA_DISK has mountpoints (system disk)
if lsblk -n -o MOUNTPOINT "$DATA_DISK" | grep -q '/'; then
echo "BŁĄD: $DATA_DISK ma partycje zamontowane — przerwanie!"
exit 1
fi
# Create partition if missing
if ! lsblk -n "$DATA_PART" &>/dev/null; then
echo "=== Partycjonowanie $DATA_DISK ==="
parted -s "$DATA_DISK" mklabel gpt
parted -s "$DATA_DISK" mkpart primary ext4 0% 100%
partprobe "$DATA_DISK"
sleep 2
fi
# Format if no filesystem
if ! blkid -o value -s TYPE "$DATA_PART" 2>/dev/null | grep -q ext4; then
echo "=== Formatowanie $DATA_PART (ext4) ==="
mkfs.ext4 -F -L data1tb "$DATA_PART"
fi
UUID=$(blkid -o value -s UUID "$DATA_PART")
echo "UUID=$UUID"
# Mount point
mkdir -p "$MOUNT_POINT"
if ! mountpoint -q "$MOUNT_POINT"; then
echo "=== Montowanie $DATA_PART -> $MOUNT_POINT ==="
mount "$DATA_PART" "$MOUNT_POINT"
fi
# fstab
FSTAB_LINE="UUID=${UUID} ${MOUNT_POINT} ext4 defaults,noatime 0 2"
if ! grep -q "$MOUNT_POINT" /etc/fstab; then
echo "=== Dodaję wpis do /etc/fstab ==="
cp /etc/fstab /etc/fstab.bak.$(date +%Y%m%d%H%M%S)
echo "$FSTAB_LINE" >> /etc/fstab
else
echo "=== fstab już zawiera $MOUNT_POINT ==="
fi
mount -a
df -h "$MOUNT_POINT"
# Directory structure
echo "=== Tworzę strukturę katalogów ==="
mkdir -p "$MOUNT_POINT/docker"
mkdir -p "$MOUNT_POINT/apps/vllm/huggingface"
mkdir -p "$MOUNT_POINT/apps/gguf/"{qwen3.6-27b,gemma-4-12b}
mkdir -p "$MOUNT_POINT/apps/localai/"{models,backends,configuration,images,data}
mkdir -p "$MOUNT_POINT/apps/comfyui/storage"
mkdir -p "$MOUNT_POINT/apps/comfyui/cache/"{hf-hub,torch-hub}
mkdir -p "$MOUNT_POINT/apps/comfyui/"{models,input,output,custom_nodes,workflows}
chown -R tomasz-syn-grzegorza:tomasz-syn-grzegorza "$MOUNT_POINT/apps"
# Docker data-root migration
echo "=== Migracja Docker data-root ==="
systemctl stop docker docker.socket 2>/dev/null || true
if [ -d /var/lib/docker ] && [ "$(ls -A /var/lib/docker 2>/dev/null)" ]; then
echo "Kopiuję /var/lib/docker -> $MOUNT_POINT/docker ..."
rsync -aHAX /var/lib/docker/ "$MOUNT_POINT/docker/"
fi
mkdir -p "$MOUNT_POINT/docker"
python3 <<'PYEOF'
import json
from pathlib import Path
p = Path("/etc/docker/daemon.json")
cfg = json.loads(p.read_text()) if p.exists() else {}
cfg["data-root"] = "/data/docker"
p.write_text(json.dumps(cfg, indent=2) + "\n")
print(p.read_text())
PYEOF
systemctl start docker
sleep 2
echo "=== Weryfikacja ==="
docker info | grep "Docker Root Dir"
df -h / "$MOUNT_POINT"
lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,MODEL
echo "=== Test Docker ==="
docker run --rm hello-world
docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu24.04 nvidia-smi
echo ""
echo "=== GOTOWE ==="
echo "Dysk danych: $MOUNT_POINT ($UUID)"
echo "Docker Root: /data/docker"
echo "Modele vLLM: /data/apps/vllm/huggingface"
echo "LocalAI: /data/apps/localai/models"
+14
View File
@@ -0,0 +1,14 @@
# Data disk mount point
DATA_ROOT=/data
# ComfyUI web UI (default ComfyUI port)
COMFYUI_PORT=8188
# yanwk/comfyui-boot — CUDA 12.6 slim (GPU in container)
COMFYUI_IMAGE=yanwk/comfyui-boot:cu126-slim
# Use only the discrete NVIDIA GPU
CUDA_VISIBLE_DEVICES=0
# Extra CLI args passed to ComfyUI (e.g. --fast)
CLI_ARGS=
+1
View File
@@ -0,0 +1 @@
.env
+134
View File
@@ -0,0 +1,134 @@
# ComfyUI stack
[ComfyUI](https://github.com/comfyanonymous/ComfyUI) — grafowy interfejs do generowania obrazów (Stable Diffusion, Flux, …). Stack oparty na obrazie [`yanwk/comfyui-boot`](https://github.com/YanWenKun/ComfyUI-Docker).
Zamiast [Stability Matrix](https://github.com/LykosAI/StabilityMatrix) (GUI desktop) używamy ComfyUI w Dockerze — zgodnie z filozofią headless serwera i tutoriala [03b](../../manual-tutorial/03b-system-tools.md).
## Porty
| Serwis | Port | URL |
|--------|------|-----|
| ComfyUI web UI | **8188** | `http://HOST:8188` |
| LocalAI (osobny stack) | 8070 | LLM / chat — **nie równolegle z dużym modelem SD** |
## Jak to działa
```mermaid
flowchart LR
browser["Przeglądarka :8188"]
comfyui["Kontener comfyui"]
gpu["RTX 3090 Ti"]
models["/data/apps/comfyui/models"]
browser --> comfyui
comfyui --> gpu
comfyui --> models
```
| Element | Opis |
|---------|------|
| Obraz | `yanwk/comfyui-boot:cu126-slim` |
| Konfiguracja | `.env` + `docker-compose.yml` |
| Modele | `/data/apps/comfyui/models` (puste na start — pobierz ręcznie lub przez ComfyUI-Manager) |
| Pierwszy start | Kopiuje ComfyUI do `/data/apps/comfyui/storage/` (~kilka minut) |
## Struktura
```
stacks/comfyui/
├── README.md
├── docker-compose.yml
├── .env.example
├── .gitignore
└── scripts/
├── ensure-dirs.sh
├── pull.sh
└── start.sh
```
Na dysku `/data`:
```
/data/apps/comfyui/
├── storage/ # kopia ComfyUI z obrazu (pierwszy start)
├── models/ # checkpoints, LoRA, VAE, …
├── cache/
│ ├── hf-hub/
│ └── torch-hub/
├── input/
├── output/
├── custom_nodes/
└── workflows/
```
## Workflow (bez modelu)
```bash
cd stacks/comfyui
cp .env.example .env
./scripts/pull.sh
./scripts/start.sh
```
Weryfikacja:
```bash
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8188/
# UI: http://<IP-serwera>:8188
```
## Zmienne `.env`
| Zmienna | Opis | Domyślnie |
|---------|------|-----------|
| `DATA_ROOT` | Mount dysku danych | `/data` |
| `COMFYUI_PORT` | Port na hoście | `8188` |
| `COMFYUI_IMAGE` | Obraz Docker | `yanwk/comfyui-boot:cu126-slim` |
| `CUDA_VISIBLE_DEVICES` | GPU | `0` |
| `CLI_ARGS` | Dodatkowe flagi ComfyUI | puste |
## Polityka GPU (LocalAI ↔ ComfyUI)
RTX 3090 Ti 24 GB — **jeden** duży workload GPU naraz.
Przed startem ComfyUI z dużym modelem (SDXL, Flux):
```bash
cd ../localai
docker compose --profile localai stop localai
```
Skrypt `start.sh` ostrzega, gdy `localai` jest uruchomiony.
W Server UI (port 8091) → Stop/Start stack `localai` lub `comfyui` według potrzeb.
## Modele (później)
- **ComfyUI-Manager** w UI (custom node w obrazie yanwk) — pobieranie modeli i węzłów
- Ręcznie: pliki do `/data/apps/comfyui/models/checkpoints/` (lub odpowiednie podkatalogi)
Szacunki VRAM (przy zatrzymanym LocalAI):
| Model | VRAM (orientacyjnie) |
|-------|----------------------|
| SD 1.5 | ~46 GB |
| SDXL | ~812 GB |
| Flux | ~1220 GB |
## Zarządzanie
```bash
docker compose --profile comfyui ps
docker compose --profile comfyui logs -f comfyui
docker compose --profile comfyui restart comfyui
docker compose --profile comfyui down
```
## Dokumentacja
- Tutorial: [manual-tutorial/07-comfyui-stack.md](../../manual-tutorial/07-comfyui-stack.md)
- Research Stability Matrix: [`coding-agent/STABILITYMATRIX-RESEARCH.md`](../../coding-agent/STABILITYMATRIX-RESEARCH.md)
- Wdrożenie: [`coding-agent/COMFYUI-DEPLOYMENT.md`](../../coding-agent/COMFYUI-DEPLOYMENT.md)
Upstream: [github.com/comfyanonymous/ComfyUI](https://github.com/comfyanonymous/ComfyUI)
+1
View File
@@ -0,0 +1 @@
docker-compose.yml
+33
View File
@@ -0,0 +1,33 @@
name: comfyui
services:
comfyui:
image: ${COMFYUI_IMAGE:-yanwk/comfyui-boot:cu126-slim}
container_name: comfyui
profiles:
- comfyui
restart: unless-stopped
init: true
ports:
- "${COMFYUI_PORT:-8188}:8188"
environment:
- CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}
- NVIDIA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
- CLI_ARGS=${CLI_ARGS:-}
volumes:
- ${DATA_ROOT:-/data}/apps/comfyui/storage:/root
- ${DATA_ROOT:-/data}/apps/comfyui/models:/root/ComfyUI/models
- ${DATA_ROOT:-/data}/apps/comfyui/cache/hf-hub:/root/.cache/huggingface/hub
- ${DATA_ROOT:-/data}/apps/comfyui/cache/torch-hub:/root/.cache/torch/hub
- ${DATA_ROOT:-/data}/apps/comfyui/input:/root/ComfyUI/input
- ${DATA_ROOT:-/data}/apps/comfyui/output:/root/ComfyUI/output
- ${DATA_ROOT:-/data}/apps/comfyui/custom_nodes:/root/ComfyUI/custom_nodes
- ${DATA_ROOT:-/data}/apps/comfyui/workflows:/root/ComfyUI/user/default/workflows
gpus: all
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8188/"]
interval: 1m
timeout: 30s
retries: 5
start_period: 3m
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Create ComfyUI data directories on the data disk.
ensure_comfyui_dirs() {
local data_root="${1:-/data}"
mkdir -p \
"${data_root}/apps/comfyui/storage" \
"${data_root}/apps/comfyui/models" \
"${data_root}/apps/comfyui/cache/hf-hub" \
"${data_root}/apps/comfyui/cache/torch-hub" \
"${data_root}/apps/comfyui/input" \
"${data_root}/apps/comfyui/output" \
"${data_root}/apps/comfyui/custom_nodes" \
"${data_root}/apps/comfyui/workflows"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
ensure_comfyui_dirs "${1:-/data}"
fi
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${STACK_DIR}"
if [[ -f .env ]]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
docker compose --profile comfyui pull
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/ensure-dirs.sh"
cd "${STACK_DIR}"
if [[ ! -f .env ]]; then
echo "ERROR: .env not found. Run: cp .env.example .env"
exit 1
fi
set -a
# shellcheck disable=SC1091
source .env
set +a
DATA_ROOT="${DATA_ROOT:-/data}"
if ! mountpoint -q "${DATA_ROOT}" 2>/dev/null; then
echo "ERROR: ${DATA_ROOT} is not mounted"
exit 1
fi
ensure_comfyui_dirs "${DATA_ROOT}"
if ! docker info &>/dev/null; then
echo "ERROR: Docker is not running"
exit 1
fi
if docker ps --format '{{.Names}}' | grep -qx localai; then
echo "UWAGA: Kontener localai jest uruchomiony."
echo " Na RTX 3090 Ti 24 GB uruchom tylko jeden duży workload GPU."
echo " Zatrzymaj LocalAI przed generowaniem obrazów:"
echo " cd ../localai && docker compose --profile localai stop localai"
echo ""
read -r -p "Kontynuować mimo to? [y/N]: " confirm
if [[ "${confirm,,}" != "y" ]]; then
exit 1
fi
fi
echo "=== ComfyUI stack ==="
echo "Image: ${COMFYUI_IMAGE:-yanwk/comfyui-boot:cu126-slim}"
echo "Port: ${COMFYUI_PORT:-8188}"
echo "Models: ${DATA_ROOT}/apps/comfyui/models"
echo "GPU: CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}"
echo ""
echo "Pierwszy start kopiuje ComfyUI do ${DATA_ROOT}/apps/comfyui/storage/ (~kilka minut)."
echo ""
docker compose --profile comfyui pull
docker compose --profile comfyui up -d
echo ""
echo "Started. Follow logs:"
echo " docker compose --profile comfyui logs -f comfyui"
echo ""
echo "Web UI (po starcie):"
echo " http://localhost:${COMFYUI_PORT:-8188}"
echo ""
echo "Health:"
echo " curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:${COMFYUI_PORT:-8188}/"
+34
View File
@@ -0,0 +1,34 @@
# Unified control plane credentials (gpu-fan + Server UI)
# Production: /opt/control-plane/.env
# Dev: cp stacks/control-plane/.env.example stacks/control-plane/.env
# --- Shared auth (Server UI panel + gpu-fan agent proxy) ---
API_KEY=change-me-generate-with-openssl-rand-hex-16
# --- Server UI ---
SERVER_UI_HOST=0.0.0.0
SERVER_UI_PORT=8091
REPO_ROOT=/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal
DOCKER_GID=999
# GPU fan agent URL (Server UI proxy target)
GPU_FAN_AGENT_URL=http://127.0.0.1:18090
# --- gpu-fan agent (NVML daemon) ---
GPU_FAN_API_HOST=127.0.0.1
GPU_FAN_API_PORT=18090
# Fan curve config (created by install.sh under /etc/gpu-fan/)
CURVE_PATH=/etc/gpu-fan/curve.json
POLL_INTERVAL=2.0
GPU_INDEX=0
# --- File explorer (Server UI) ---
FILE_EXPLORER_ROOT=/
FILE_EXPLORER_MAX_BYTES=2097152
# --- CLI terminal (Server UI) ---
CLI_ENABLED=1
CLI_SHELL=/bin/bash
CLI_DEFAULT_CWD=/home/tomasz-syn-grzegorza
CLI_MAX_SESSIONS=5
+1
View File
@@ -0,0 +1 @@
.env
+127
View File
@@ -0,0 +1,127 @@
"""Load unified control-plane environment from file + os.environ."""
from __future__ import annotations
import os
from pathlib import Path
_ENV_OVERRIDE_KEYS = (
"API_KEY",
"SERVER_UI_HOST",
"SERVER_UI_PORT",
"REPO_ROOT",
"DOCKER_GID",
"GPU_FAN_AGENT_URL",
"GPU_FAN_API_HOST",
"GPU_FAN_API_PORT",
"GPU_FAN_HOST",
"GPU_FAN_PORT",
"CURVE_PATH",
"POLL_INTERVAL",
"GPU_INDEX",
"DRY_RUN",
"FILE_EXPLORER_ROOT",
"FILE_EXPLORER_MAX_BYTES",
"CLI_ENABLED",
"CLI_SHELL",
"CLI_DEFAULT_CWD",
"CLI_MAX_SESSIONS",
)
def _parse_env_file(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not path.is_file():
return values
try:
text = path.read_text(encoding="utf-8")
except OSError:
# e.g. /opt/control-plane/.env is root-only; systemd still injects via os.environ
return values
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
values[key.strip()] = val.strip()
return values
def _merge_into(target: dict[str, str], source: dict[str, str]) -> None:
for key, val in source.items():
if val:
target[key] = val
def _is_production_stack_dir(stack_dir: Path) -> bool:
try:
return stack_dir.resolve().parts[1:2] == ("opt",)
except IndexError:
return False
def control_plane_env_paths(stack_dir: Path) -> list[Path]:
"""Candidate .env files in load order (earlier = lower priority among files)."""
paths: list[Path] = []
custom = os.environ.get("CONTROL_PLANE_ENV", "").strip()
if custom:
paths.append(Path(custom))
if _is_production_stack_dir(stack_dir):
# Production (/opt/server-ui, /opt/gpu-fan): single canonical file only.
prod = Path("/opt/control-plane/.env")
if prod not in paths:
paths.append(prod)
docker_repo = Path("/repo/stacks/control-plane/.env")
if docker_repo.is_file() and docker_repo not in paths:
paths.append(docker_repo)
return paths
# Dev (repo stacks/*): stacks/control-plane/.env only — no legacy per-service .env.
repo_control_plane = stack_dir.parent / "control-plane" / ".env"
if repo_control_plane.is_file() and repo_control_plane not in paths:
paths.append(repo_control_plane)
docker_repo = Path("/repo/stacks/control-plane/.env")
if docker_repo.is_file() and docker_repo not in paths:
paths.append(docker_repo)
return paths
def api_key_source(stack_dir: Path, values: dict[str, str]) -> str:
"""Human-readable hint for logs (no secret values)."""
if os.environ.get("API_KEY"):
return "systemd/os.environ"
for path in control_plane_env_paths(stack_dir):
parsed = _parse_env_file(path)
if parsed.get("API_KEY"):
return str(path)
if values.get("API_KEY"):
return "merged env"
return "not configured"
def load_control_plane_env(stack_dir: Path) -> dict[str, str]:
"""Merge env files then apply os.environ (systemd EnvironmentFile wins)."""
values: dict[str, str] = {}
for path in control_plane_env_paths(stack_dir):
_merge_into(values, _parse_env_file(path))
for key in _ENV_OVERRIDE_KEYS:
if key in os.environ:
values[key] = os.environ[key]
values.setdefault("GPU_FAN_AGENT_URL", "http://127.0.0.1:18090")
return values
def ensure_control_plane_import_path() -> None:
"""Add /opt/control-plane to sys.path for production imports."""
import sys
opt = "/opt/control-plane"
if opt not in sys.path:
sys.path.insert(0, opt)
repo = Path(__file__).resolve().parent
repo_str = str(repo)
if repo_str not in sys.path:
sys.path.insert(0, repo_str)
+2
View File
@@ -0,0 +1,2 @@
# DEPRECATED — use stacks/control-plane/.env.example
# Copy: cp ../control-plane/.env.example ../control-plane/.env
+4
View File
@@ -0,0 +1,4 @@
.env
.venv/
__pycache__/
*.pyc
+154
View File
@@ -0,0 +1,154 @@
# GPU Fan Control stack
Sterowanie wentylatorami **RTX 3090 Ti** na headless Ubuntu przez NVML — bez `nvidia-settings` i bez GUI.
**Panel webowy** jest w **Server UI** (`http://<host>:8091` → zakładka **GPU Fan**). Ten stack uruchamia tylko **agenta API** na localhost.
## Porty
| Serwis | Port | Dostęp |
|--------|------|--------|
| GPU Fan agent API | **18090** | `127.0.0.1` tylko (systemd, root) |
| GPU Fan UI | — | Server UI **:8091** (zakładka GPU Fan) |
Port **8090** nie jest już używany w produkcji.
## Jak to działa
```mermaid
flowchart LR
browser["Przeglądarka :8091"]
serverUI["server-ui"]
agent["fan_daemon.py :18090"]
nvml["NVML / pynvml"]
gpu["RTX 3090 Ti"]
browser --> serverUI
serverUI -->|"proxy /api/gpu-fan"| agent
agent --> nvml
nvml --> gpu
```
| Element | Opis |
|---------|------|
| Agent | `fan_daemon.py` — pętla NVML + API REST |
| Dev UI | `app.py` — monolit z UI (tylko dev / DRY_RUN) |
| Logika | `fan_controller.py` — interpolacja krzywej |
| Konfiguracja | `/etc/gpu-fan/curve.json` |
| systemd | `gpu-fan.service` (root, auto-restart) |
## Wymagania
- Sterownik NVIDIA ≥ 520 (testowane: **595-server-open**)
- Root / sudo (zapis NVML wymaga uprawnień root)
- Python 3 + venv (instalowane przez `install.sh`)
- **Server UI** na :8091 (proxy do agenta)
## Szybki start
```bash
cd stacks/gpu-fan
cp ../control-plane/.env.example ../control-plane/.env
sudo scripts/install.sh
sudo systemctl start gpu-fan
cd ../server-ui
sudo scripts/install.sh
```
Panel: `http://<IP-serwera>:8091` → zakładka **GPU Fan**.
Po zmianach w kodzie:
```bash
sudo scripts/install.sh && sudo systemctl restart gpu-fan
cd ../server-ui && sudo scripts/install.sh && sudo systemctl restart server-ui
```
Konfiguracja: produkcja `/opt/control-plane/.env`, dev `stacks/control-plane/.env`.
### Klucz API
Jeden wspólny `API_KEY` w `/opt/control-plane/.env` — auth panelu Server UI i agenta gpu-fan (proxy `/api/gpu-fan/*`).
W panelu Server UI wpisz ten sam klucz w polu **API Key** (lub `?api_key=...` w URL).
## Preset max cooling
Domyślna krzywa w [`curve.default.json`](curve.default.json):
| Temp | Speed |
|------|-------|
| 30°C | 50% |
| 40°C | 65% |
| 50°C | 80% |
| 55°C | 90% |
| 60°C | 100% |
| 70°C+ | 100% |
## Tryby
| Tryb | Opis |
|------|------|
| `curve` | Krzywa z JSON — interpolacja liniowa |
| `manual` | Stała prędkość (np. 100% awaryjnie) |
| `auto` | Oddaje sterowanie driverowi NVIDIA |
## API agenta (localhost :18090)
| Endpoint | Metoda | Opis |
|----------|--------|------|
| `/api/status` | GET | Temperatura, wentylatory, moc, tryb |
| `/api/curve` | GET/PUT | Odczyt / zapis krzywej |
| `/api/mode` | POST | `{"mode":"auto\|curve\|manual","speed":100}` |
| `/api/reload` | POST | Przeładuj `curve.json` (jak `SIGHUP`) |
Z LAN używaj proxy Server UI: `/api/gpu-fan/status`, `/api/gpu-fan/curve`, itd.
## Ograniczenia NVIDIA
- `nvidia-smi` **nie steruje** wentylatorami — tylko odczyt
- API akceptuje **0%** (= auto) lub **30100%**
- Po `systemctl stop gpu-fan` wentylatory wracają do trybu auto drivera
## Struktura
```
stacks/gpu-fan/
├── README.md
├── fan_daemon.py # produkcja — agent API
├── app.py # dev — UI + API (opcjonalnie)
├── fan_controller.py
├── curve.default.json
├── requirements.txt
├── gpu-fan.service
├── .env.example
├── static/index.html # referencja UI (wbudowane w server-ui)
└── scripts/
├── install.sh
├── enable-lan.sh # konfiguruje agent + wskazówka :8091
└── status.sh
```
## Troubleshooting
**`Insufficient Permissions`** — uruchom jako root (`sudo systemctl start gpu-fan`).
**Panel GPU Fan pusty / 502** — sprawdź agent: `curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: ..."`
**Port 18090 zajęty**`scripts/status.sh` / `sudo scripts/status.sh --cleanup`
**Przeładuj krzywą bez restartu:**
```bash
sudo systemctl reload gpu-fan
```
## Dokumentacja
| Dokument | Opis |
|----------|------|
| [docs/00-START-TUTAJ.md](docs/00-START-TUTAJ.md) | Mapa — zacznij tutaj |
| [docs/02-OTWIERANIE-UI-W-PRZEGLADARCE.md](docs/02-OTWIERANIE-UI-W-PRZEGLADARCE.md) | Panel w Server UI :8091 |
| [docs/05-DLACZEGO-NIE-DOCKER.md](docs/05-DLACZEGO-NIE-DOCKER.md) | Dlaczego host, nie Docker |
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""GPU fan control web UI + NVML daemon (single process)."""
from __future__ import annotations
import logging
import os
import signal
import sys
import threading
from pathlib import Path
from typing import Any
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse
from pydantic import BaseModel, Field
from fan_controller import (
FanControlError,
FanController,
MAX_CURVE_POINTS,
MIN_CURVE_POINTS,
MIN_FAN_SPEED,
MAX_FAN_SPEED,
curve_to_dict,
parse_curve,
)
STACK_DIR = Path(__file__).resolve().parent
# Unified control-plane env (see stacks/control-plane/env_loader.py)
for _cp in ("/opt/control-plane", "/repo/stacks/control-plane", str(STACK_DIR.parent / "control-plane")):
if _cp not in sys.path and Path(_cp).exists():
sys.path.insert(0, _cp)
from env_loader import load_control_plane_env # noqa: E402
STATIC_DIR = STACK_DIR / "static"
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
log = logging.getLogger(__name__)
def _is_localhost_bind(host: str) -> bool:
return host.strip().lower() in ("127.0.0.1", "localhost", "::1")
def _resolve_host_port(env: dict[str, str]) -> tuple[str, int]:
host = env.get("GPU_FAN_API_HOST") or env.get("GPU_FAN_HOST", "0.0.0.0")
port_raw = env.get("GPU_FAN_API_PORT") or env.get("GPU_FAN_PORT", "8090")
return host, int(port_raw)
ENV = load_control_plane_env(STACK_DIR)
HOST, PORT = _resolve_host_port(ENV)
API_KEY = ENV.get("API_KEY", "")
CURVE_PATH = Path(ENV.get("CURVE_PATH", "/etc/gpu-fan/curve.json"))
POLL_INTERVAL = float(ENV.get("POLL_INTERVAL", "2.0"))
GPU_INDEX = int(ENV.get("GPU_INDEX", "0"))
DRY_RUN = ENV.get("DRY_RUN", "").lower() in ("1", "true", "yes")
controller = FanController(
curve_path=CURVE_PATH,
gpu_index=GPU_INDEX,
poll_interval=POLL_INTERVAL,
)
controller.dry_run = DRY_RUN
app = FastAPI(title="GPU Fan Control", version="1.0.0")
class CurvePoint(BaseModel):
temp: int = Field(ge=0, le=120)
speed: int = Field(ge=0, le=100)
class CurveUpdate(BaseModel):
points: list[CurvePoint] = Field(min_length=MIN_CURVE_POINTS, max_length=MAX_CURVE_POINTS)
class ModeUpdate(BaseModel):
mode: str
speed: int | None = Field(default=None, ge=MIN_FAN_SPEED, le=MAX_FAN_SPEED)
def require_auth(request: Request) -> None:
if not API_KEY:
return
key = request.headers.get("X-API-Key", "")
if key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid or missing API key")
@app.get("/")
def index() -> FileResponse:
return FileResponse(STATIC_DIR / "index.html")
@app.get("/api/status")
def api_status(_: None = Depends(require_auth)) -> dict[str, Any]:
return controller.get_metrics()
@app.get("/api/curve")
def api_get_curve(_: None = Depends(require_auth)) -> dict[str, Any]:
points = controller.get_curve()
return {"points": [{"temp": t, "speed": s} for t, s in points]}
@app.put("/api/curve")
def api_put_curve(body: CurveUpdate, _: None = Depends(require_auth)) -> dict[str, Any]:
points = [(p.temp, p.speed) for p in body.points]
try:
parse_curve(curve_to_dict(points))
controller.save_curve_file(points)
controller.set_mode("curve")
except FanControlError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "curve": curve_to_dict(controller.get_curve())}
@app.post("/api/mode")
def api_set_mode(body: ModeUpdate, _: None = Depends(require_auth)) -> dict[str, Any]:
try:
controller.set_mode(body.mode, body.speed)
if body.mode != "auto":
controller.update_once()
except FanControlError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "mode": controller.mode, "manual_speed": controller.manual_speed}
@app.post("/api/reload")
def api_reload(_: None = Depends(require_auth)) -> dict[str, Any]:
try:
controller.reload_curve()
except FanControlError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "curve": curve_to_dict(controller.get_curve())}
def run_daemon_thread() -> None:
controller.run_loop()
def main() -> None:
if os.geteuid() != 0 and not DRY_RUN:
log.error("GPU fan control requires root (NVML write access). Run with sudo.")
sys.exit(1)
if not _is_localhost_bind(HOST) and not API_KEY:
log.error(
"API_KEY is required when GPU_FAN_HOST=%s (LAN/public bind). "
"Set API_KEY in .env",
HOST,
)
sys.exit(1)
controller.init_nvml()
def shutdown_handler(signum: int, _frame: object) -> None:
log.info("Received signal %s", signum)
if signum == signal.SIGHUP:
try:
controller.reload_curve()
except FanControlError as exc:
log.error("Curve reload failed: %s", exc)
return
controller.shutdown()
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGHUP, shutdown_handler)
thread = threading.Thread(target=run_daemon_thread, daemon=True)
thread.start()
if _is_localhost_bind(HOST):
log.info("Web UI at http://127.0.0.1:%d", PORT)
else:
log.info("Web UI listening on 0.0.0.0:%d (LAN — API key required)", PORT)
uvicorn.run(app, host=HOST, port=PORT, log_level="info")
if __name__ == "__main__":
if os.environ.get("GPU_FAN_LEGACY_UI", "").lower() in ("1", "true", "yes"):
main()
else:
from fan_daemon import main as daemon_main
daemon_main()
@@ -0,0 +1,228 @@
# GPU Fan — Docker vs host (raport techniczny)
**Data:** 2026-07-04
**Stack:** `stacks/gpu-fan/`
**Host docelowy:** gmktec-k11, RTX 3090 Ti, Ubuntu headless
---
## 1. Executive summary
**GPU Fan musi działać na hoście jako usługa systemd (root), nie w Dockerze.**
Aplikacja steruje wentylatorami karty NVIDIA przez zapis do NVML (`nvmlDeviceSetFanSpeed_v2`, `nvmlDeviceSetFanControlPolicy`). Na kartach GeForce wymaga to uprawnień root i bezpośredniego dostępu do sterownika hosta. Repo nie zawiera Dockerfile ani compose dla tego stacku — jedyny wspierany model to `sudo scripts/install.sh``/opt/gpu-fan` + `gpu-fan.service`.
Docker jest teoretycznie możliwy (privileged container, host network, mount `/dev/nvidia*`), ale kruchy, nieutrzymywany i niezgodny z architekturą ubuntu-bare-metal (gpu-fan jako daemon sprzętowy obok workloadów AI w kontenerach).
---
## 2. Co robi aplikacja
| Komponent | Plik | Rola |
|-----------|------|------|
| Pętla sterowania | `fan_controller.py` | Odczyt temp/mocy, interpolacja krzywej, zapis prędkości wentylatorów |
| Web UI + API | `app.py` | FastAPI na porcie **8090**, wątek daemon NVML |
| UI statyczne | `static/index.html` | Wykres krzywej, status live, edycja trybu |
| Krzywa | `/etc/gpu-fan/curve.json` | Mapowanie temp °C → speed % |
### Tryby pracy
| Tryb | Zachowanie |
|------|------------|
| `curve` | Prędkość z krzywej JSON (interpolacja liniowa, 37 punktów) |
| `manual` | Stała prędkość 30100% |
| `auto` | Przywraca politykę drivera NVIDIA (`NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW`) |
### API (port 8090)
| Endpoint | Metoda | Uwagi |
|----------|--------|-------|
| `/` | GET | Web UI |
| `/api/status` | GET | Metryki GPU + tryb |
| `/api/curve` | GET/PUT | Odczyt/zapis krzywej |
| `/api/mode` | POST | Zmiana trybu |
| `/api/reload` | POST | Przeładowanie `curve.json` (jak SIGHUP) |
Nagłówek `X-API-Key` wymagany gdy `GPU_FAN_HOST` ≠ localhost (domyślnie LAN bind `0.0.0.0`).
### Shutdown
Przy `SIGTERM` / `SIGINT` kontroler wywołuje `_restore_auto_policy()` przed `nvmlShutdown()` — wentylatory nie zostają w trybie manual po zatrzymaniu usługi.
---
## 3. Zależności sprzętowe i software
| Zależność | Wymagana | Uwagi |
|-----------|----------|-------|
| NVIDIA driver ≥ 520 | Tak | Testowane: 595-server-open |
| `nvidia-ml-py` (pynvml) | Tak | Jedyny interfejs sterowania w kodzie |
| `nvidia-smi` | Nie w kodzie | Tylko weryfikacja w dokumentacji; **nie ustawia** wentylatorów |
| Root (euid 0) | Tak | `app.py` kończy się błędem bez root (chyba że `DRY_RUN=true`) |
| `nvidia-persistenced` | Zalecane | `gpu-fan.service` After=/Wants= |
| IPMI | Nie | Brak referencji w kodzie |
| D-Bus | Nie | Brak referencji |
| X11 / nvidia-settings | Nie | Headless — celowo unikane |
| Python 3 + venv | Tak | FastAPI, uvicorn |
### Ścieżki produkcyjne
| Ścieżka | Zawartość |
|---------|-----------|
| `/opt/gpu-fan/` | Kod aplikacji (rsync z repo przez `install.sh`) |
| `/opt/control-plane/.env` | `API_KEY`, `GPU_FAN_API_*`, `CURVE_PATH`, … |
| `/etc/gpu-fan/curve.json` | Krzywa temp → speed |
| `/etc/systemd/system/gpu-fan.service` | Unit systemd |
**Uwaga:** `stacks/control-plane/.env` w repo ≠ `/opt/control-plane/.env``setup-control-plane-env.sh` migruje i synchronizuje.
---
## 4. Obecny model wdrożenia
```
repo stacks/gpu-fan/
│ sudo scripts/install.sh
/opt/gpu-fan/ ← kod + .venv + .env
/etc/gpu-fan/curve.json
/etc/systemd/system/gpu-fan.service
│ systemctl enable --now gpu-fan
Proces root: python app.py
├── wątek: fan_controller.run_loop() (co POLL_INTERVAL s)
└── uvicorn: 0.0.0.0:8090
```
Skrypty pomocnicze:
| Skrypt | Cel |
|--------|-----|
| `scripts/install.sh` | Instalacja produkcyjna |
| `scripts/enable-lan.sh` | `GPU_FAN_HOST=0.0.0.0`, API_KEY, restart |
| `scripts/start.sh` | Foreground debug (wymaga stop systemd) |
| `scripts/status.sh` | Diagnostyka portu/procesu |
| `scripts/self-test.sh` | Test krzywej, NVML read, API dry-run |
---
## 5. Analiza Docker — dlaczego nie
### Brak artefaktów w repo
- Brak `Dockerfile`, `compose.yaml`, profilu w `server-ui/stacks.yaml`
- Inne stacki GPU (ComfyUI, LocalAI, vLLM) używają Docker; gpu-fan jest wyjątkiem celowym
### Blokery techniczne
| Bloker | Szczegóły |
|--------|-----------|
| NVML write na GeForce | `nvmlDeviceSetFanSpeed_v2` wymaga root; kontenery GPU (`NVIDIA_DRIVER_CAPABILITIES=compute,utility`) nie gwarantują zapisu fan policy |
| Coupling do host driver | Wersja NVML w kontenerze musi pasować do kernel drivera hosta |
| Lifecycle | `docker kill` / crash kontenera może pominąć `_restore_auto_policy()` → wentylatory w manual |
| `nvidia-persistenced` | Daemon na hoście; kontener nie zarządza persystencją GPU |
| Privileged + host network | Minimalny „Docker” wyglądałby jak host install z dodatkową warstwą — bez korzyści |
### Hipotetyczny kontener (nie implementować)
Gdyby ktoś eksperymentował:
```yaml
# NIE WDRAŻAĆ — tylko dokumentacja ryzyka
privileged: true
network_mode: host
user: root
pid: host # opcjonalnie, nadal ryzykowne
volumes:
- /etc/gpu-fan:/etc/gpu-fan
devices:
- /dev/nvidia0
- /dev/nvidiactl
- /dev/nvidia-uvm
```
Nawet wtedy sukces nie jest gwarantowany na RTX 3090 Ti; repo nie będzie tego utrzymywać.
---
## 6. Współistnienie z Docker AI stacks
```
┌─────────────────────────────────────────────────┐
│ Host (gmktec-k11) │
│ │
│ gpu-fan.service (root, :8090) ──NVML──► GPU │
│ │
│ ┌─────────────┐ ┌──────────┐ ┌──────────┐ │
│ │ comfyui │ │ localai │ │ vllm │ │
│ │ :8188 │ │ :8070 │ │ :8000 │ │
│ └─────────────┘ └──────────┘ └──────────┘ │
│ Docker containers (GPU compute) │
└─────────────────────────────────────────────────┘
```
- ComfyUI/LocalAI obciążają GPU → temperatura rośnie → gpu-fan reaguje krzywą
- Zatrzymaj gpu-fan **nie** jest wymagane przed startem kontenerów AI
- Polityka GPU w Server UI (jeden duży workload) dotyczy LLM/SD, nie gpu-fan
- Firewall (NPMPlus): port 8090 nie powinien być publiczny; LAN + API key
Źródło: `manual-tutorial/06-gpu-fan-control.md` — „gpu-fan.service na hoście (NVML, root)”.
---
## 7. Checklist operacyjny (coding-agent)
### Instalacja / upgrade
- [ ] `nvidia-smi` działa
- [ ] `sudo scripts/install.sh` z katalogu `stacks/gpu-fan`
- [ ] `sudo scripts/enable-lan.sh` jeśli dostęp z LAN
- [ ] `sudo systemctl enable --now gpu-fan`
- [ ] `curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: $(grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)"` → JSON z `temperature_c`
### Po zmianie kodu
```bash
sudo scripts/install.sh && sudo systemctl restart gpu-fan
```
### Diagnostyka
```bash
systemctl status gpu-fan
journalctl -u gpu-fan -f
scripts/status.sh
sudo scripts/status.sh --cleanup # tylko gdy port zajęty przez osierocony proces
```
### Czego nie robić
- Nie uruchamiać `start.sh` i systemd jednocześnie (port 8090)
- Nie pakować gpu-fan do Docker bez nowego ADR i testów na sprzęcie
- Nie edytować tylko `stacks/control-plane/.env` — produkcja czyta `/opt/control-plane/.env`
---
## 8. Rekomendacja
| Decyzja | Uzasadnienie |
|---------|--------------|
| **Zostaw na hoście (systemd)** | Wspierane, przetestowane, bezpieczny shutdown, zgodne z tutorial 06 |
| **Nie dodawaj Docker** | Brak wartości, wysokie ryzyko, duplikacja root access |
| **Dokumentacja użytkownika** | `docs/` — kroki instalacji i troubleshooting |
| **Ten raport** | `coding-agent/DOCKER-VS-HOST-REPORT.md` — odniesienie dla agentów |
---
## 9. Pliki źródłowe (indeks)
| Plik | Kluczowe fragmenty |
|------|-------------------|
| `app.py:161-164` | Wymóg root |
| `fan_controller.py:250-279` | NVML fan policy + speed write |
| `fan_controller.py:335-343` | Shutdown → auto policy |
| `gpu-fan.service` | User=root, After=nvidia-persistenced |
| `scripts/install.sh` | rsync → /opt/gpu-fan |
| `requirements.txt` | fastapi, uvicorn, nvidia-ml-py |
+8
View File
@@ -0,0 +1,8 @@
{
"30": 50,
"40": 65,
"50": 80,
"55": 90,
"60": 100,
"70": 100
}
+8
View File
@@ -0,0 +1,8 @@
{
"30": 50,
"40": 65,
"50": 80,
"55": 90,
"60": 100,
"70": 100
}
+56
View File
@@ -0,0 +1,56 @@
# GPU Fan — START TUTAJ
**Cel:** Wiedzieć od czego zacząć i czy ta aplikacja idzie do Dockera.
**Czas:** 2 minuty czytania
**Wymagania:** Brak (to tylko mapa dokumentacji)
---
## Jednozdaniowa odpowiedź
**GPU Fan NIE działa w Dockerze — instalujesz go na hoście (systemd), raz, i zapominasz.**
Steruje wentylatorami karty graficznej. Reszta (ComfyUI, LocalAI) może być w Dockerze — to osobne programy.
---
## Mapa dokumentacji
| Plik | Kiedy czytać |
|------|----------------|
| [01-INSTALACJA-KROK-PO-KROKU.md](01-INSTALACJA-KROK-PO-KROKU.md) | Pierwsza instalacja na serwerze |
| [02-OTWIERANIE-UI-W-PRZEGLADARCE.md](02-OTWIERANIE-UI-W-PRZEGLADARCE.md) | Chcesz otworzyć panel w przeglądarce |
| [03-KRZYWa-I-TRYBY.md](03-KRZYWa-I-TRYBY.md) | Chcesz zmienić chłodzenie / tryb wentylatorów |
| [04-CZESTE-BLEDY.md](04-CZESTE-BLEDY.md) | Coś nie działa |
| [05-DLACZEGO-NIE-DOCKER.md](05-DLACZEGO-NIE-DOCKER.md) | „A czemu nie w kontenerze?” |
Dla agentów AI / deweloperów: [../coding-agent/DOCKER-VS-HOST-REPORT.md](../coding-agent/DOCKER-VS-HOST-REPORT.md)
---
## Szybka ścieżka (jeśli się spieszysz)
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/gpu-fan
nvidia-smi # musi pokazać kartę — jeśli nie, najpierw napraw driver
sudo scripts/install.sh
sudo scripts/enable-lan.sh
sudo systemctl start gpu-fan
sudo systemctl status gpu-fan
```
Potem: [02-OTWIERANIE-UI-W-PRZEGLADARCE.md](02-OTWIERANIE-UI-W-PRZEGLADARCE.md)
---
## Co zobaczysz gdy działa
- `systemctl status gpu-fan`**active (running)**
- W przeglądarce: panel z temperaturą GPU i wykresem krzywej (port **8090**)
- Wentylatory reagują na temperaturę (w trybie **curve**)
---
## Co zrobić gdy nie działa
→ [04-CZESTE-BLEDY.md](04-CZESTE-BLEDY.md)
@@ -0,0 +1,127 @@
# Instalacja GPU Fan — krok po kroku
**Cel:** Zainstalować sterowanie wentylatorami GPU na serwerze.
**Czas:** ~10 minut
**Wymagania:** `sudo`, działający `nvidia-smi`, internet (pip)
---
## Krok 0 — Sprawdź kartę graficzną
Na serwerze wpisz:
```bash
nvidia-smi
```
**Co zobaczysz gdy OK:** tabela z nazwą karty (np. RTX 3090 Ti), driverem, temperaturą.
**Gdy nie działa:** najpierw napraw sterownik NVIDIA. Bez tego GPU Fan nie ma sensu.
---
## Krok 1 — Wejdź do katalogu stacku
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/gpu-fan
```
(Ścieżka może być inna — ważne żebyś był w folderze z plikiem `app.py`.)
---
## Krok 2 — Zainstaluj na hoście (NIE Docker)
```bash
sudo scripts/install.sh
```
Skrypt:
- kopiuje pliki do `/opt/gpu-fan`
- tworzy `/etc/gpu-fan/curve.json` (domyślna krzywa chłodzenia)
- tworzy `/opt/control-plane/.env` z losowym `API_KEY` (przy pierwszej instalacji)
- instaluje usługę systemd `gpu-fan`
**Co zobaczysz gdy OK:** komunikat „Installed to /opt/gpu-fan” i instrukcja startu.
---
## Krok 3 — Włącz dostęp z sieci lokalnej (LAN)
```bash
sudo scripts/enable-lan.sh
```
Ustawia nasłuch na `0.0.0.0:8090` i upewnia się że jest `API_KEY`.
---
## Krok 4 — Uruchom usługę
```bash
sudo systemctl start gpu-fan
sudo systemctl enable gpu-fan
```
`enable` = start automatyczny po restarcie serwera.
---
## Krok 5 — Sprawdź czy działa
```bash
sudo systemctl status gpu-fan
```
**Co zobaczysz gdy OK:** `Active: active (running)` na zielono.
Logi na żywo:
```bash
journalctl -u gpu-fan -f
```
(Wyjdź: Ctrl+C)
Test API (skopiuj całość):
```bash
API_KEY=$(grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)
curl -s http://127.0.0.1:8090/api/status -H "X-API-Key: $API_KEY" | head -c 200
```
**Co zobaczysz gdy OK:** JSON z `"temperature_c"` i `"mode"`.
---
## Krok 6 — Otwórz panel w przeglądarce
→ [02-OTWIERANIE-UI-W-PRZEGLADARCE.md](02-OTWIERANIE-UI-W-PRZEGLADARCE.md)
---
## Po aktualizacji kodu w repo
Jeśli zmieniłeś pliki w `stacks/gpu-fan/`:
```bash
cd ~/cursor/ubuntu-bare-metal/stacks/gpu-fan
sudo scripts/install.sh
sudo systemctl restart gpu-fan
```
---
## Czego NIE robić
| Nie rób tego | Dlaczego |
|--------------|----------|
| `sudo scripts/start.sh` + `systemctl start gpu-fan` naraz | Dwa programy na porcie 8090 — błąd |
| Instalacja w Dockerze | Nie wspierane — patrz [05-DLACZEGO-NIE-DOCKER.md](05-DLACZEGO-NIE-DOCKER.md) |
| Edycja tylko `stacks/control-plane/.env` | Produkcja czyta `/opt/control-plane/.env` |
---
## Co zrobić gdy nie działa
→ [04-CZESTE-BLEDY.md](04-CZESTE-BLEDY.md)
@@ -0,0 +1,100 @@
# Otwieranie panelu GPU Fan w przeglądarce
**Cel:** Wejść do panelu sterowania wentylatorami GPU z komputera w sieci LAN.
**Czas:** ~3 minuty
**Wymagania:** Działające usługi `gpu-fan` (agent) i `server-ui` (panel)
> UI gpu-fan **nie** jest już na porcie 8090. Użyj **Server UI** na porcie **8091**, zakładka **GPU Fan**.
---
## Krok 1 — Sprawdź IP serwera
Na serwerze:
```bash
hostname -I | awk '{print $1}'
```
Przykład wyniku: `192.168.100.90` — to Twój adres w LAN.
---
## Krok 2 — Pobierz klucz API
Na serwerze:
```bash
grep ^API_KEY= /opt/control-plane/.env
# lub ten sam klucz z agenta:
grep ^API_KEY= /opt/control-plane/.env
```
Przykład: `API_KEY=a1b2c3d4e5f6...` — skopiuj część **po** znaku `=`.
---
## Krok 3 — Otwórz URL w przeglądarce
Z innego komputera w tej samej sieci WiFi/LAN:
```
http://192.168.100.90:8091/?api_key=WKLEJ_TUTAJ_KLUCZ#gpu-fan
```
Zamień:
- `192.168.100.90` → IP z kroku 1
- `WKLEJ_TUTAJ_KLUCZ` → wartość z kroku 2
Kliknij zakładkę **GPU Fan** (lub użyj `#gpu-fan` w URL).
**Co zobaczysz gdy OK:** wykres krzywej wentylatorów, temperatura GPU, monitoring mocy/VRAM.
Klucz zapisze się w przeglądarce (localStorage `server-ui-api-key`) — przy kolejnych wizytach wystarczy `http://IP:8091/#gpu-fan`.
---
## Dostęp tylko z samego serwera
```
http://127.0.0.1:8091/#gpu-fan
```
(API key może być wymagany gdy `SERVER_UI_HOST=0.0.0.0`.)
---
## Dostęp przez SSH (opcjonalnie)
Na **swoim** laptopie:
```bash
ssh -L 8091:127.0.0.1:8091 TWOJ_USER@192.168.100.90
```
W przeglądarce na laptopie: `http://localhost:8091/#gpu-fan`
---
## Co zrobić gdy nie działa
| Objaw | Co zrobić |
|-------|-----------|
| Strona się nie ładuje | `sudo systemctl status server-ui` — czy **running**? |
| Zakładka GPU Fan pusta / błąd 502 | `sudo systemctl status gpu-fan` — agent na :18090 |
| `401` / brak danych | Zły klucz API — sprawdź `/opt/control-plane/.env` |
| Agent nie odpowiada | `curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: KLUCZ"` |
Test proxy:
```bash
curl -s http://127.0.0.1:8091/api/gpu-fan/health -H "X-API-Key: $(grep ^API_KEY= /opt/control-plane/.env | cut -d= -f2)"
```
---
## Bezpieczeństwo (krótko)
- Panel **nie** wystawiaj na internet bez firewall / VPN.
- W LAN używaj `API_KEY` w Server UI.
- Agent gpu-fan (:18090) nasłuchuje tylko na localhost.
+112
View File
@@ -0,0 +1,112 @@
# Krzywa temperatury i tryby wentylatorów
**Cel:** Zrozumieć jak GPU Fan reguluje wentylatory i kiedy zmienić tryb.
**Czas:** ~5 minut
**Wymagania:** Działający panel (patrz [02-OTWIERANIE-UI-W-PRZEGLADARCE.md](02-OTWIERANIE-UI-W-PRZEGLADARCE.md))
---
## Trzy tryby — który wybrać
| Tryb | Kiedy używać | Co robi |
|------|--------------|---------|
| **curve** | Normalna praca (domyślny) | Im wyższa temp GPU, tym szybsze wentylatory — według krzywej |
| **manual** | Awaryjnie: „na full” lub stała prędkość | Ustawiasz np. 100% ręcznie |
| **auto** | Chcesz oddać sterowanie driverowi NVIDIA | Jak bez GPU Fan — driver sam decyduje |
**Rekomendacja:** zostaw **curve** przy obciążeniu AI (ComfyUI, LocalAI). Użyj **manual 100%** tylko gdy karta się przegrzewa i chcesz na chwilę max chłodzenia.
---
## Jak działa krzywa (curve)
Plik: `/etc/gpu-fan/curve.json`
Przykład (uproszczony):
```json
{
"30": 50,
"40": 65,
"50": 80,
"55": 90,
"60": 100,
"70": 100
}
```
Znaczenie: przy **50°C** wentylatory ~**80%**, przy **60°C** i wyżej → **100%**.
Między punktami program **interpoluje** (płynna zmiana).
### Zasady (ważne)
- **3 do 7** punktów
- Temperatury **rosnąco**, każda **unikalna**
- Prędkość: **0** (= auto w API) albo **30100%** (wartości 129 są niedozwolone)
---
## Domyślna krzywa „max cooling”
Po instalacji masz agresywne chłodzenie (bezpieczne dla RTX 3090 Ti pod obciążeniem):
| Temp GPU | Wentylatory |
|----------|-------------|
| 30°C | 50% |
| 40°C | 65% |
| 50°C | 80% |
| 55°C | 90% |
| 60°C+ | 100% |
Źródło: `curve.default.json` w repo → kopiowane do `/etc/gpu-fan/curve.json`.
---
## Edycja w panelu web
1. Otwórz UI (port 8090)
2. Zmień punkty na wykresie / w tabeli
3. Kliknij **Zapisz** — zapisuje do `/etc/gpu-fan/curve.json` i włącza tryb **curve**
---
## Edycja z terminala
```bash
sudo nano /etc/gpu-fan/curve.json
sudo systemctl reload gpu-fan
```
`reload` przeładowuje plik bez pełnego restartu.
---
## Co zobaczysz w panelu
- **Temperatura** — aktualna temp GPU
- **Fan speeds** — odczyt z karty (%)
- **Target** — co program próbuje ustawić (w trybie curve/manual)
- **Mode** — curve / manual / auto
---
## Po zatrzymaniu usługi
```bash
sudo systemctl stop gpu-fan
```
Wentylatory wracają do trybu **auto** drivera NVIDIA — to zamierzone (bezpieczeństwo).
---
## Co zrobić gdy wentylatory „dziwnie” się zachowują
| Sytuacja | Wyjaśnienie |
|----------|-------------|
| 0% przy niskiej temp w trybie **auto** | Normalne — karta wyłącza wentylatory przy idle |
| Głośno od razu w **curve** | Domyślna krzywa jest agresywna — obniż prędkości w JSON |
| Brak reakcji | Sprawdź tryb — czy na pewno **curve**, nie **auto** |
Więcej: [04-CZESTE-BLEDY.md](04-CZESTE-BLEDY.md)
+163
View File
@@ -0,0 +1,163 @@
# Częste błędy GPU Fan
**Cel:** Naprawić typowe problemy bez zgadywania.
**Czas:** zależy od problemu (215 min)
**Wymagania:** Dostęp SSH do serwera, `sudo`
---
## Szybka diagnostyka (zrób to najpierw)
```bash
sudo systemctl status gpu-fan
nvidia-smi
scripts/status.sh
```
Skopiuj wynik jeśli dalej nie działa.
---
## Błąd: `Insufficient Permissions` w logach
**Przyczyna:** Program nie działa jako root.
**Naprawa:**
```bash
sudo systemctl restart gpu-fan
sudo systemctl status gpu-fan
```
Nie uruchamiaj `python app.py` bez sudo (chyba że `DRY_RUN=true` tylko do testów).
---
## Błąd: `address already in use` / port 8090 zajęty
**Przyczyna:** Dwie kopie programu naraz (najczęściej systemd + `scripts/start.sh`).
**Naprawa:**
```bash
sudo systemctl stop gpu-fan
sudo scripts/status.sh --cleanup
sudo systemctl start gpu-fan
```
**Nie zmieniaj portu na 8091** — to maskuje problem, nie go rozwiązuje.
---
## Błąd: Strona w przeglądarce się nie ładuje
| Sprawdź | Komenda |
|---------|---------|
| Usługa działa? | `sudo systemctl status gpu-fan` |
| Port nasłuchuje? | `ss -tlnp \| grep 8090` |
| LAN włączony? | `grep GPU_FAN_HOST /opt/control-plane/.env` → powinno być `0.0.0.0` |
| Firewall? | Upewnij się że LAN może dojść do 8090 |
Włącz LAN:
```bash
sudo scripts/enable-lan.sh
```
---
## Błąd: `401` / Invalid API key
**Przyczyna:** Zły klucz w URL lub brak nagłówka.
**Naprawa:**
```bash
grep ^API_KEY= /opt/control-plane/.env
```
Użyj w przeglądarce:
```
http://IP_SERWERA:8090/?api_key=KLUCZ_Z_PLIKU
```
Patrz: [02-OTWIERANIE-UI-W-PRZEGLADARCE.md](02-OTWIERANIE-UI-W-PRZEGLADARCE.md)
---
## Błąd: Zmieniam `.env` w repo i nic się nie dzieje
**Przyczyna:** Produkcja czyta **`/opt/control-plane/.env`**, nie `stacks/control-plane/.env`.
**Naprawa:**
```bash
sudo nano /opt/control-plane/.env
sudo systemctl restart gpu-fan
```
Albo pełna reinstalacja kodu (nie nadpisuje istniejącego `.env`):
```bash
sudo scripts/install.sh
```
---
## Błąd: `nvidia-smi` nie działa
**Przyczyna:** Brak / zły sterownik NVIDIA.
GPU Fan **nie naprawi** drivera. Najpierw napraw GPU w systemie, potem wróć do instalacji.
---
## Błąd: Po Ctrl+Z w terminalu port zajęty
**Przyczyna:** Proces zawieszony w tle nadal trzyma port.
**Naprawa:**
```bash
jobs -l # zobacz zawieszone
kill %1 # numer z jobs
# albo:
sudo scripts/status.sh --cleanup
```
---
## Błąd: Krzywa się nie zapisuje
Sprawdź format JSON — 37 punktów, temp unikalne, speed 0 lub 30100.
```bash
sudo cat /etc/gpu-fan/curve.json
journalctl -u gpu-fan -n 30
```
---
## Błąd: Chcę uruchomić w Dockerze
**Odpowiedź:** Nie rób tego. Patrz [05-DLACZEGO-NIE-DOCKER.md](05-DLACZEGO-NIE-DOCKER.md).
---
## Pełny reset (ostateczność)
```bash
sudo systemctl stop gpu-fan
sudo scripts/status.sh --cleanup
cd ~/cursor/ubuntu-bare-metal/stacks/gpu-fan
sudo scripts/install.sh
sudo scripts/enable-lan.sh
sudo systemctl start gpu-fan
```
Logi:
```bash
journalctl -u gpu-fan -f
```
@@ -0,0 +1,78 @@
# Dlaczego GPU Fan NIE jest w Dockerze
**Cel:** Zrozumieć dlaczego instalujemy na hoście, a nie jak ComfyUI.
**Czas:** 3 minuty
**Wymagania:** Brak
---
## Krótka odpowiedź
GPU Fan **musi** siedzieć na hoście obok sterownika NVIDIA. W Dockerze **nie ma** gotowego obrazu w tym repo i **nie planujemy** go dodawać.
ComfyUI / LocalAI = programy w kontenerach (obliczenia AI).
GPU Fan = „termometr + regulator wentylatorów” na poziomie sprzętu.
---
## Analogia
| Co | Gdzie | Dlaczego |
|----|-------|----------|
| Aplikacja w przeglądarce | Docker OK | Nie dotyka wentylatorów bezpośrednio |
| Sterownik wentylatorów karty | Host (root) | Tylko system z uprawnieniami root może pisać do NVML |
To jak termostat w kotłowni — nie wkładasz go do pudełka z aplikacją webową w innym pokoju.
---
## Co by się stało w Dockerze (gdyby ktoś próbował)
1. Kontener musiałby być **privileged** i **root**
2. I tak często dostaniesz **Insufficient Permissions** na kartach GeForce
3. Po awaryjnym `docker kill` wentylatory mogłyby zostać w trybie ręcznym
4. Zero korzyści — i tak potrzebujesz tego samego sterownika na hoście
Dlatego w repo jest tylko:
```bash
sudo scripts/install.sh
sudo systemctl start gpu-fan
```
---
## Jak to wygląda na serwerze (poprawnie)
```
HOST (Ubuntu)
├── gpu-fan.service ← steruje wentylatorami (port 8090)
├── sterownik NVIDIA
└── Docker
├── comfyui :8188
├── localai :8070
└── vllm :8000
```
Wszystko może działać **równocześnie**. GPU Fan nie koliduje z kontenerami AI.
---
## „Ale inne stacki są w Dockerze!”
Tak — bo tam Docker **ma sens** (izolacja aplikacji, GPU do inference).
GPU Fan to **daemon sprzętowy** — jak `nvidia-persistenced`, nie jak aplikacja webowa.
---
## Gdzie jest pełna analiza techniczna
Dla agentów / deweloperów:
[../coding-agent/DOCKER-VS-HOST-REPORT.md](../coding-agent/DOCKER-VS-HOST-REPORT.md)
---
## Co robić zamiast Dockera
→ [01-INSTALACJA-KROK-PO-KROKU.md](01-INSTALACJA-KROK-PO-KROKU.md)
+358
View File
@@ -0,0 +1,358 @@
"""NVML fan control with JSON curve, modes, and graceful shutdown."""
from __future__ import annotations
import json
import logging
import signal
import threading
import time
from pathlib import Path
from typing import Any
import pynvml
log = logging.getLogger(__name__)
MIN_CURVE_POINTS = 3
MAX_CURVE_POINTS = 7
MIN_FAN_SPEED = 30
MAX_FAN_SPEED = 100
class FanControlError(Exception):
pass
def _clamp_speed(speed: int) -> int:
if speed == 0:
return 0
return max(MIN_FAN_SPEED, min(MAX_FAN_SPEED, speed))
def parse_curve(raw: dict[str, Any]) -> list[tuple[int, int]]:
if not raw:
raise FanControlError("Curve is empty")
points: list[tuple[int, int]] = []
for temp_str, speed in raw.items():
try:
temp = int(temp_str)
speed_int = int(speed)
except (TypeError, ValueError) as exc:
raise FanControlError(f"Invalid curve point {temp_str!r}: {speed!r}") from exc
if not 0 <= temp <= 120:
raise FanControlError(f"Temperature {temp} out of range 0-120")
if speed_int != 0 and not MIN_FAN_SPEED <= speed_int <= MAX_FAN_SPEED:
raise FanControlError(
f"Fan speed {speed_int}% invalid (use 0 or {MIN_FAN_SPEED}-{MAX_FAN_SPEED})"
)
points.append((temp, speed_int))
points.sort(key=lambda p: p[0])
temps = [p[0] for p in points]
if len(set(temps)) != len(temps):
raise FanControlError("Temperatures must be unique")
if len(points) < MIN_CURVE_POINTS or len(points) > MAX_CURVE_POINTS:
raise FanControlError(
f"Curve must have {MIN_CURVE_POINTS}-{MAX_CURVE_POINTS} points"
)
return points
def curve_to_dict(points: list[tuple[int, int]]) -> dict[str, int]:
return {str(temp): speed for temp, speed in points}
def interpolate_speed(temp: int, curve: list[tuple[int, int]]) -> int:
if temp <= curve[0][0]:
return curve[0][1]
if temp >= curve[-1][0]:
return curve[-1][1]
for i in range(len(curve) - 1):
t1, s1 = curve[i]
t2, s2 = curve[i + 1]
if t1 <= temp <= t2:
if t2 == t1:
return s2
ratio = (temp - t1) / (t2 - t1)
return int(s1 + ratio * (s2 - s1))
return curve[-1][1]
class FanController:
def __init__(
self,
curve_path: Path,
gpu_index: int = 0,
poll_interval: float = 2.0,
) -> None:
self.curve_path = curve_path
self.gpu_index = gpu_index
self.poll_interval = poll_interval
self._lock = threading.Lock()
self._running = False
self._mode = "curve"
self._manual_speed = 100
self._curve: list[tuple[int, int]] = []
self._handle: Any = None
self._fan_count = 0
self._gpu_name = ""
self._last_metrics: dict[str, Any] = {}
self._auto_active = False
self.dry_run = False
@property
def mode(self) -> str:
with self._lock:
return self._mode
@property
def manual_speed(self) -> int:
with self._lock:
return self._manual_speed
def load_curve_file(self) -> list[tuple[int, int]]:
if not self.curve_path.exists():
raise FanControlError(f"Curve file not found: {self.curve_path}")
raw = json.loads(self.curve_path.read_text(encoding="utf-8"))
return parse_curve(raw)
def save_curve_file(self, points: list[tuple[int, int]]) -> None:
validated = parse_curve(curve_to_dict(points))
self.curve_path.parent.mkdir(parents=True, exist_ok=True)
self.curve_path.write_text(
json.dumps(curve_to_dict(validated), indent=2) + "\n",
encoding="utf-8",
)
with self._lock:
self._curve = validated
log.info("Saved curve: %s", validated)
def reload_curve(self) -> None:
curve = self.load_curve_file()
with self._lock:
self._curve = curve
log.info("Reloaded curve: %s", curve)
def set_mode(self, mode: str, manual_speed: int | None = None) -> None:
if mode not in ("auto", "curve", "manual"):
raise FanControlError(f"Unknown mode: {mode}")
with self._lock:
self._mode = mode
if manual_speed is not None:
self._manual_speed = _clamp_speed(manual_speed)
log.info("Mode set to %s (manual_speed=%s)", mode, manual_speed)
def get_curve(self) -> list[tuple[int, int]]:
with self._lock:
return list(self._curve)
def get_metrics(self) -> dict[str, Any]:
with self._lock:
return dict(self._last_metrics)
def init_nvml(self) -> None:
pynvml.nvmlInit()
count = pynvml.nvmlDeviceGetCount()
if self.gpu_index >= count:
raise FanControlError(
f"GPU index {self.gpu_index} out of range (found {count} GPU(s))"
)
handle = pynvml.nvmlDeviceGetHandleByIndex(self.gpu_index)
name = pynvml.nvmlDeviceGetName(handle)
fan_count = pynvml.nvmlDeviceGetNumFans(handle)
self._handle = handle
self._fan_count = fan_count
self._gpu_name = name if isinstance(name, str) else name.decode()
self.reload_curve()
log.info("GPU %d: %s (%d fan(s))", self.gpu_index, self._gpu_name, fan_count)
def _read_metrics(self) -> dict[str, Any]:
handle = self._handle
assert handle is not None
temp = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
power_mw = pynvml.nvmlDeviceGetPowerUsage(handle)
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
memory_used_mb = mem_info.used // (1024**2)
memory_total_mb = mem_info.total // (1024**2)
power_limit_w: float | None = None
clock_graphics_mhz: int | None = None
clock_memory_mhz: int | None = None
try:
power_limit_w = round(
pynvml.nvmlDeviceGetEnforcedPowerLimit(handle) / 1000, 1
)
except pynvml.NVMLError:
pass
try:
clock_graphics_mhz = pynvml.nvmlDeviceGetClockInfo(
handle, pynvml.NVML_CLOCK_GRAPHICS
)
except pynvml.NVMLError:
pass
try:
clock_memory_mhz = pynvml.nvmlDeviceGetClockInfo(
handle, pynvml.NVML_CLOCK_MEM
)
except pynvml.NVMLError:
pass
fan_speeds: list[int] = []
for fan_idx in range(self._fan_count):
try:
fan_speeds.append(pynvml.nvmlDeviceGetFanSpeed_v2(handle, fan_idx))
except pynvml.NVMLError:
fan_speeds.append(-1)
with self._lock:
mode = self._mode
manual_speed = self._manual_speed
curve = list(self._curve)
if mode == "curve":
target = interpolate_speed(temp, curve)
elif mode == "manual":
target = manual_speed
else:
target = None
metrics = {
"gpu_index": self.gpu_index,
"gpu_name": self._gpu_name,
"temperature_c": temp,
"fan_speeds_pct": fan_speeds,
"target_speed_pct": target,
"power_w": round(power_mw / 1000, 1),
"power_limit_w": power_limit_w,
"utilization_pct": util.gpu,
"memory_utilization_pct": util.memory,
"memory_used_mb": memory_used_mb,
"memory_total_mb": memory_total_mb,
"clock_graphics_mhz": clock_graphics_mhz,
"clock_memory_mhz": clock_memory_mhz,
"mode": mode,
"manual_speed_pct": manual_speed,
"curve": curve_to_dict(curve),
}
with self._lock:
self._last_metrics = metrics
return metrics
def _set_manual_policy(self) -> None:
handle = self._handle
assert handle is not None
for fan_idx in range(self._fan_count):
pynvml.nvmlDeviceSetFanControlPolicy(
handle, fan_idx, pynvml.NVML_FAN_POLICY_MANUAL
)
def _restore_auto_policy(self) -> None:
if self._handle is None:
return
log.info("Restoring automatic fan control...")
for fan_idx in range(self._fan_count):
try:
pynvml.nvmlDeviceSetFanControlPolicy(
self._handle,
fan_idx,
pynvml.NVML_FAN_POLICY_TEMPERATURE_CONTINOUS_SW,
)
log.info("Fan %d: restored to auto", fan_idx)
except pynvml.NVMLError as exc:
log.error("Fan %d: could not restore auto: %s", fan_idx, exc)
def _apply_fan_speed(self, speed: int) -> None:
handle = self._handle
assert handle is not None
speed = _clamp_speed(speed)
self._set_manual_policy()
for fan_idx in range(self._fan_count):
pynvml.nvmlDeviceSetFanSpeed_v2(handle, fan_idx, speed)
def update_once(self) -> dict[str, Any]:
metrics = self._read_metrics()
mode = metrics["mode"]
if mode == "auto":
if not self._auto_active and not self.dry_run:
self._restore_auto_policy()
self._auto_active = True
elif self.dry_run:
self._auto_active = True
return metrics
self._auto_active = False
target = metrics["target_speed_pct"]
if target is None:
return metrics
if self.dry_run:
log.info(
"[dry-run] GPU %d: %d°C -> %d%% (fans: %s)",
self.gpu_index,
metrics["temperature_c"],
target,
metrics["fan_speeds_pct"],
)
return metrics
try:
self._apply_fan_speed(target)
log.info(
"GPU %d: %d°C -> %d%% (fans: %s)",
self.gpu_index,
metrics["temperature_c"],
target,
metrics["fan_speeds_pct"],
)
except pynvml.NVMLError as exc:
log.error("Failed to set fan speed: %s", exc)
metrics["error"] = str(exc)
return metrics
def run_loop(self) -> None:
self._running = True
log.info("Fan control loop started (interval=%ss)", self.poll_interval)
while self._running:
try:
self.update_once()
except Exception:
log.exception("Fan control loop error")
time.sleep(self.poll_interval)
def stop(self) -> None:
self._running = False
def shutdown(self) -> None:
self.stop()
if not self.dry_run:
self._restore_auto_policy()
try:
pynvml.nvmlShutdown()
except pynvml.NVMLError:
pass
log.info("Fan controller shut down")
def install_signal_handlers(self) -> None:
def handler(signum: int, _frame: Any) -> None:
log.info("Received signal %s", signum)
if signum == signal.SIGHUP:
try:
self.reload_curve()
except FanControlError as exc:
log.error("Curve reload failed: %s", exc)
return
self.shutdown()
signal.signal(signal.SIGTERM, handler)
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGHUP, handler)
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
"""GPU fan control NVML daemon + API only (no web UI)."""
from __future__ import annotations
import logging
import os
import signal
import sys
import threading
from pathlib import Path
from typing import Any
import uvicorn
from fastapi import Depends, FastAPI, HTTPException, Request
from pydantic import BaseModel, Field
from fan_controller import (
FanControlError,
FanController,
MAX_CURVE_POINTS,
MIN_CURVE_POINTS,
MIN_FAN_SPEED,
MAX_FAN_SPEED,
curve_to_dict,
parse_curve,
)
STACK_DIR = Path(__file__).resolve().parent
# Unified control-plane env (see stacks/control-plane/env_loader.py)
for _cp in ("/opt/control-plane", "/repo/stacks/control-plane", str(STACK_DIR.parent / "control-plane")):
if _cp not in sys.path and Path(_cp).exists():
sys.path.insert(0, _cp)
from env_loader import load_control_plane_env # noqa: E402
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
log = logging.getLogger(__name__)
def _resolve_host_port(env: dict[str, str]) -> tuple[str, int]:
"""Agent API bind — prefer GPU_FAN_API_*; never fall back to legacy LAN :8090."""
if env.get("GPU_FAN_API_HOST") or env.get("GPU_FAN_API_PORT"):
host = env.get("GPU_FAN_API_HOST", "127.0.0.1")
port_raw = env.get("GPU_FAN_API_PORT", "18090")
else:
host = "127.0.0.1"
port_raw = "18090"
return host, int(port_raw)
def _is_localhost_bind(host: str) -> bool:
return host.strip().lower() in ("127.0.0.1", "localhost", "::1")
ENV = load_control_plane_env(STACK_DIR)
HOST, PORT = _resolve_host_port(ENV)
API_KEY = ENV.get("API_KEY", "")
CURVE_PATH = Path(ENV.get("CURVE_PATH", "/etc/gpu-fan/curve.json"))
POLL_INTERVAL = float(ENV.get("POLL_INTERVAL", "2.0"))
GPU_INDEX = int(ENV.get("GPU_INDEX", "0"))
DRY_RUN = ENV.get("DRY_RUN", "").lower() in ("1", "true", "yes")
controller = FanController(
curve_path=CURVE_PATH,
gpu_index=GPU_INDEX,
poll_interval=POLL_INTERVAL,
)
controller.dry_run = DRY_RUN
app = FastAPI(title="GPU Fan Agent", version="1.0.0")
class CurvePoint(BaseModel):
temp: int = Field(ge=0, le=120)
speed: int = Field(ge=0, le=100)
class CurveUpdate(BaseModel):
points: list[CurvePoint] = Field(min_length=MIN_CURVE_POINTS, max_length=MAX_CURVE_POINTS)
class ModeUpdate(BaseModel):
mode: str
speed: int | None = Field(default=None, ge=MIN_FAN_SPEED, le=MAX_FAN_SPEED)
def require_auth(request: Request) -> None:
if not API_KEY:
return
key = request.headers.get("X-API-Key", "")
if key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid or missing API key")
@app.get("/api/status")
def api_status(_: None = Depends(require_auth)) -> dict[str, Any]:
return controller.get_metrics()
@app.get("/api/curve")
def api_get_curve(_: None = Depends(require_auth)) -> dict[str, Any]:
points = controller.get_curve()
return {"points": [{"temp": t, "speed": s} for t, s in points]}
@app.put("/api/curve")
def api_put_curve(body: CurveUpdate, _: None = Depends(require_auth)) -> dict[str, Any]:
points = [(p.temp, p.speed) for p in body.points]
try:
parse_curve(curve_to_dict(points))
controller.save_curve_file(points)
controller.set_mode("curve")
except FanControlError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "curve": curve_to_dict(controller.get_curve())}
@app.post("/api/mode")
def api_set_mode(body: ModeUpdate, _: None = Depends(require_auth)) -> dict[str, Any]:
try:
controller.set_mode(body.mode, body.speed)
if body.mode != "auto":
controller.update_once()
except FanControlError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "mode": controller.mode, "manual_speed": controller.manual_speed}
@app.post("/api/reload")
def api_reload(_: None = Depends(require_auth)) -> dict[str, Any]:
try:
controller.reload_curve()
except FanControlError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return {"ok": True, "curve": curve_to_dict(controller.get_curve())}
def run_daemon_thread() -> None:
controller.run_loop()
def main() -> None:
if os.geteuid() != 0 and not DRY_RUN:
log.error("GPU fan control requires root (NVML write access). Run with sudo.")
sys.exit(1)
if not _is_localhost_bind(HOST) and not API_KEY:
log.error(
"API_KEY is required when GPU_FAN_API_HOST=%s (LAN/public bind). "
"Set API_KEY in .env",
HOST,
)
sys.exit(1)
controller.init_nvml()
def shutdown_handler(signum: int, _frame: object) -> None:
log.info("Received signal %s", signum)
if signum == signal.SIGHUP:
try:
controller.reload_curve()
except FanControlError as exc:
log.error("Curve reload failed: %s", exc)
return
controller.shutdown()
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown_handler)
signal.signal(signal.SIGINT, shutdown_handler)
signal.signal(signal.SIGHUP, shutdown_handler)
thread = threading.Thread(target=run_daemon_thread, daemon=True)
thread.start()
log.info("GPU fan agent API at http://%s:%d", HOST, PORT)
log.info("Web UI: use Server UI at :8091 (GPU Fan tab)")
uvicorn.run(app, host=HOST, port=PORT, log_level="info")
if __name__ == "__main__":
main()
+18
View File
@@ -0,0 +1,18 @@
[Unit]
Description=GPU Fan Control (NVML agent API)
After=nvidia-persistenced.service
Wants=nvidia-persistenced.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/gpu-fan
EnvironmentFile=-/opt/control-plane/.env
ExecStart=/opt/gpu-fan/.venv/bin/python /opt/gpu-fan/fan_daemon.py
Restart=on-failure
RestartSec=5
KillSignal=SIGTERM
TimeoutStopSec=15
[Install]
WantedBy=multi-user.target
+3
View File
@@ -0,0 +1,3 @@
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
nvidia-ml-py>=12.560.0
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Configure gpu-fan agent for localhost + ensure API key. UI is in Server UI :8091.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVER_UI_DIR="$(cd "${STACK_DIR}/../server-ui" && pwd)"
CONTROL_PLANE_ENV="/opt/control-plane/.env"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo ${SCRIPT_DIR}/enable-lan.sh"
exit 1
fi
"${SCRIPT_DIR}/install.sh"
set_env_var() {
local file="$1" key="$2" val="$3"
if grep -q "^${key}=" "${file}"; then
sed -i "s|^${key}=.*|${key}=${val}|" "${file}"
else
echo "${key}=${val}" >> "${file}"
fi
}
for pair in "GPU_FAN_API_HOST=127.0.0.1" "GPU_FAN_API_PORT=18090"; do
set_env_var "${CONTROL_PLANE_ENV}" "${pair%%=*}" "${pair#*=}"
done
if ! grep -q '^API_KEY=.\{8,\}' "${CONTROL_PLANE_ENV}"; then
KEY="$(openssl rand -hex 16)"
set_env_var "${CONTROL_PLANE_ENV}" "API_KEY" "${KEY}"
echo "Generated new API_KEY in ${CONTROL_PLANE_ENV}"
else
echo "API_KEY already set in ${CONTROL_PLANE_ENV}"
fi
systemctl restart gpu-fan
sleep 2
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
echo ""
echo "gpu-fan agent: $(systemctl is-active gpu-fan)"
echo "Agent API: http://127.0.0.1:18090 (localhost only)"
echo ""
echo "GPU Fan panel (LAN):"
echo " http://${LAN_IP:-<server-ip>}:8091 → zakładka GPU Fan"
echo ""
echo "API key (Server UI + gpu-fan):"
echo " grep ^API_KEY= ${CONTROL_PLANE_ENV}"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVER_UI_DIR="$(cd "${STACK_DIR}/../server-ui" && pwd)"
INSTALL_DIR="/opt/gpu-fan"
CONFIG_DIR="/etc/gpu-fan"
SERVICE_NAME="gpu-fan.service"
AGENT_PORT=18090
CONTROL_PLANE_ENV="/opt/control-plane/.env"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo ${SCRIPT_DIR}/install.sh"
exit 1
fi
echo "=== GPU Fan Control — install ==="
bash "${SERVER_UI_DIR}/scripts/setup-control-plane-env.sh"
apt-get update -qq
apt-get install -y python3-venv python3-pip
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}"
rsync -a --delete \
--exclude '.venv' \
--exclude '.env' \
--exclude '__pycache__' \
"${STACK_DIR}/" "${INSTALL_DIR}/"
if [[ ! -f "${CONFIG_DIR}/curve.json" ]]; then
cp "${INSTALL_DIR}/curve.default.json" "${CONFIG_DIR}/curve.json"
echo "Installed default curve: ${CONFIG_DIR}/curve.json"
fi
python3 -m venv "${INSTALL_DIR}/.venv"
"${INSTALL_DIR}/.venv/bin/pip" install --upgrade pip -q
"${INSTALL_DIR}/.venv/bin/pip" install -r "${INSTALL_DIR}/requirements.txt" -q
install -m 644 "${INSTALL_DIR}/gpu-fan.service" "/etc/systemd/system/${SERVICE_NAME}"
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
systemctl restart "${SERVICE_NAME}"
sleep 2
API_KEY_VAL="$(grep '^API_KEY=' "${CONTROL_PLANE_ENV}" | cut -d= -f2- || true)"
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
echo ""
echo "Installed to ${INSTALL_DIR}"
echo "Service: $(systemctl is-active "${SERVICE_NAME}" 2>/dev/null || echo unknown)"
echo "Env: ${CONTROL_PLANE_ENV}"
echo "Curve config: ${CONFIG_DIR}/curve.json"
echo "Agent API: 127.0.0.1:${AGENT_PORT} (localhost only)"
echo ""
echo "GPU Fan UI (Server UI, LAN):"
echo " http://${LAN_IP:-<server-ip>}:8091/#gpu-fan"
echo ""
echo "API key:"
echo " grep ^API_KEY= ${CONTROL_PLANE_ENV}"
echo ""
if ss -tlnp 2>/dev/null | grep -q ":${AGENT_PORT}"; then
echo "Port ${AGENT_PORT}: listening"
if [[ -n "${API_KEY_VAL}" ]]; then
STATUS="$(curl -sf "http://127.0.0.1:${AGENT_PORT}/api/status" -H "X-API-Key: ${API_KEY_VAL}" 2>/dev/null | head -c 120 || true)"
echo "Agent status: ${STATUS}..."
fi
else
echo "WARN: port ${AGENT_PORT} not listening — check: journalctl -u ${SERVICE_NAME} -n 20"
fi
echo ""
echo "Logs:"
echo " journalctl -u ${SERVICE_NAME} -f"
echo ""
echo "Pełna instalacja (gpu-fan + Server UI):"
echo " sudo ${SERVER_UI_DIR}/scripts/install-control-plane.sh"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Self-test: curve logic, NVML read, API (dry-run). Full fan write needs sudo.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${STACK_DIR}"
mkdir -p config
cp -n curve.default.json config/curve.json 2>/dev/null || true
export CURVE_PATH="${STACK_DIR}/config/curve.json"
export DRY_RUN=true
export GPU_FAN_PORT=18090
echo "=== 1. Curve logic ==="
.venv/bin/python -c "
from fan_controller import parse_curve, interpolate_speed
pts = parse_curve({'30':50,'40':65,'50':80,'55':90,'60':100,'70':100})
assert interpolate_speed(45, pts) in (72, 73)
assert interpolate_speed(70, pts) == 100
print('OK')
"
echo "=== 2. NVML read ==="
.venv/bin/python -c "
import pynvml
pynvml.nvmlInit()
h = pynvml.nvmlDeviceGetHandleByIndex(0)
t = pynvml.nvmlDeviceGetTemperature(h, pynvml.NVML_TEMPERATURE_GPU)
print(f'GPU temp: {t}C')
pynvml.nvmlShutdown()
print('OK')
"
echo "=== 3. API dry-run (background) ==="
.venv/bin/python app.py &
APP_PID=$!
trap 'kill $APP_PID 2>/dev/null || true' EXIT
sleep 2
STATUS=$(curl -sf "http://127.0.0.1:${GPU_FAN_PORT}/api/status")
echo "$STATUS" | .venv/bin/python -m json.tool | head -20
TARGET=$(echo "$STATUS" | .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['target_speed_pct'])")
TEMP=$(echo "$STATUS" | .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['temperature_c'])")
echo "Temp=${TEMP}C target=${TARGET}%"
curl -sf -X PUT "http://127.0.0.1:${GPU_FAN_PORT}/api/curve" \
-H 'Content-Type: application/json' \
-d '{"points":[{"temp":30,"speed":50},{"temp":40,"speed":65},{"temp":50,"speed":80},{"temp":55,"speed":90},{"temp":60,"speed":100},{"temp":70,"speed":100}]}' \
| .venv/bin/python -m json.tool >/dev/null
echo "Curve PUT: OK"
curl -sf -X POST "http://127.0.0.1:${GPU_FAN_PORT}/api/mode" \
-H 'Content-Type: application/json' \
-d '{"mode":"manual","speed":100}' >/dev/null
echo "Mode manual 100%: OK"
kill $APP_PID 2>/dev/null || true
trap - EXIT
echo ""
if sudo -n true 2>/dev/null; then
echo "=== 4. Fan write (root) ==="
sudo -n env CURVE_PATH="$CURVE_PATH" .venv/bin/python -c "
from pathlib import Path
from fan_controller import FanController
c = FanController(Path('$CURVE_PATH'), 0, 2.0)
c.init_nvml()
c.set_mode('manual', 50)
m = c.update_once()
print(f\"Applied: {m['temperature_c']}C -> {m['target_speed_pct']}%\")
c.set_mode('auto')
c.update_once()
c.shutdown()
print('OK')
"
else
echo "=== 4. Fan write (root) — SKIP (sudo needs password) ==="
echo " Run: sudo scripts/install.sh && sudo systemctl start gpu-fan"
fi
echo ""
echo "All automated checks passed."
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONTROL_PLANE_ENV="${STACK_DIR}/../control-plane/.env"
EXAMPLE="${STACK_DIR}/../control-plane/.env.example"
cd "${STACK_DIR}"
if [[ ! -f "${CONTROL_PLANE_ENV}" ]]; then
cp "${EXAMPLE}" "${CONTROL_PLANE_ENV}"
echo "Created ${CONTROL_PLANE_ENV} from example"
fi
set -a
# shellcheck disable=SC1091
source "${CONTROL_PLANE_ENV}"
set +a
export CONTROL_PLANE_ENV="${CONTROL_PLANE_ENV}"
CURVE_PATH="${CURVE_PATH:-/etc/gpu-fan/curve.json}"
export CURVE_PATH
if [[ ! -f "${CURVE_PATH}" ]]; then
if [[ "${EUID}" -eq 0 ]]; then
mkdir -p "$(dirname "${CURVE_PATH}")"
cp curve.default.json "${CURVE_PATH}"
else
mkdir -p "${STACK_DIR}/config"
CURVE_PATH="${STACK_DIR}/config/curve.json"
export CURVE_PATH
[[ -f "${CURVE_PATH}" ]] || cp curve.default.json "${CURVE_PATH}"
echo "Using local curve: ${CURVE_PATH}"
fi
fi
if [[ ! -d .venv ]]; then
python3 -m venv .venv
.venv/bin/pip install -q -r requirements.txt
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "ERROR: Fan control requires root. Use:"
echo " sudo -E ${SCRIPT_DIR}/start.sh"
echo "Or install system-wide:"
echo " sudo ${SCRIPT_DIR}/install.sh && sudo systemctl start gpu-fan"
exit 1
fi
if systemctl is-active --quiet gpu-fan 2>/dev/null; then
echo "ERROR: gpu-fan.service is already running (systemd)."
echo " Use: sudo systemctl stop gpu-fan"
echo " Or: sudo journalctl -u gpu-fan -f"
echo ""
echo "Do not run scripts/start.sh alongside systemd — one instance only."
exit 1
fi
PORT="${GPU_FAN_API_PORT:-18090}"
HOST="${GPU_FAN_API_HOST:-127.0.0.1}"
if ss -tln "sport = :${PORT}" 2>/dev/null | grep -q LISTEN; then
echo "ERROR: Port ${PORT} is already in use."
echo ""
ss -tlnp "sport = :${PORT}" 2>/dev/null || ss -tln "sport = :${PORT}"
echo ""
echo "Common causes:"
echo " - Suspended foreground start (Ctrl+Z) — run: jobs -l && kill %<n>"
echo " - Orphan process — run: sudo ${SCRIPT_DIR}/status.sh"
echo " - systemd still stopping — wait a few seconds"
exit 1
fi
echo "=== GPU Fan Control (foreground) ==="
echo "Agent API: http://${HOST}:${PORT}"
echo "Curve: ${CURVE_PATH}"
echo ""
echo "Stop with Ctrl+C (not Ctrl+Z — suspended process keeps the port)."
echo ""
exec .venv/bin/python app.py
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Diagnose gpu-fan port conflicts and running instances.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
OPT_ENV="/opt/control-plane/.env"
REPO_ENV="${STACK_DIR}/../control-plane/.env"
echo "=== gpu-fan status ==="
echo ""
if systemctl list-unit-files gpu-fan.service &>/dev/null; then
echo "systemd:"
systemctl is-active gpu-fan 2>/dev/null && systemctl status gpu-fan --no-pager -l 2>/dev/null | head -8 || echo " inactive"
else
echo "systemd: gpu-fan.service not installed"
fi
echo ""
echo "Ports 80908099:"
if ss -tlnp 2>/dev/null | grep -E ':809[0-9]' ; then
:
elif ss -tln 2>/dev/null | grep -E ':809[0-9]' ; then
echo " (run as root for process names: sudo ss -tlnp)"
else
echo " (none listening)"
fi
echo ""
echo "gpu-fan processes:"
pgrep -af '/opt/gpu-fan/.venv/bin/python|stacks/gpu-fan/.venv/bin/python|gpu-fan/app.py' 2>/dev/null \
|| echo " (none)"
echo ""
echo "Shell suspended jobs (Ctrl+Z):"
if jobs -l 2>/dev/null | grep -q .; then
jobs -l
echo " Kill with: kill %<job-number>"
else
echo " (none in this shell)"
fi
echo ""
echo "GPU_FAN_API_PORT config:"
[[ -f "${OPT_ENV}" ]] && echo " /opt/control-plane/.env: $(grep '^GPU_FAN_API_PORT=' "${OPT_ENV}" || echo '(not set)')"
[[ -f "${REPO_ENV}" ]] && echo " repo control-plane: $(grep '^GPU_FAN_API_PORT=' "${REPO_ENV}" || echo '(not set)')"
echo " Note: production uses /opt/control-plane/.env (shared with Server UI)"
echo ""
if [[ "${1:-}" == "--cleanup" ]]; then
if [[ "${EUID}" -ne 0 ]]; then
echo "ERROR: --cleanup requires root: sudo ${SCRIPT_DIR}/status.sh --cleanup"
exit 1
fi
echo "=== cleanup ==="
systemctl stop gpu-fan 2>/dev/null || true
pkill -f '/opt/gpu-fan/.venv/bin/python /opt/gpu-fan/app.py' 2>/dev/null || true
pkill -f 'stacks/gpu-fan/.venv/bin/python app.py' 2>/dev/null || true
sleep 0.5
if ss -tln 2>/dev/null | grep -qE ':809[0-9]'; then
echo "WARNING: ports still in use:"
ss -tlnp 2>/dev/null | grep -E ':809[0-9]' || ss -tln | grep -E ':809[0-9]'
echo "Check suspended jobs in other shells: jobs -l"
else
echo "Ports 809x are free."
fi
echo ""
echo "Start production instance:"
echo " sudo systemctl start gpu-fan"
echo "Or foreground debug (systemd stopped):"
echo " sudo ${SCRIPT_DIR}/start.sh"
fi
+755
View File
@@ -0,0 +1,755 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GPU Fan Control — RTX 3090 Ti</title>
<style>
:root {
--bg: #0d1117;
--panel: #161b22;
--border: #30363d;
--text: #e6edf3;
--muted: #8b949e;
--accent: #58a6ff;
--green: #3fb950;
--orange: #d29922;
--red: #f85149;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
h1 { margin: 0; font-size: 1.25rem; font-weight: 600; }
.subtitle { color: var(--muted); font-size: 0.875rem; }
main { padding: 1.5rem; max-width: 1100px; margin: 0 auto; }
.grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 1rem;
}
@media (max-width: 900px) {
.grid { grid-template-columns: 1fr; }
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem;
}
.card h2 {
margin: 0 0 1rem;
font-size: 0.95rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.stats { display: grid; gap: 0.75rem; }
.stat {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.stat-value { font-size: 1.5rem; font-weight: 600; }
.stat-label { color: var(--muted); font-size: 0.85rem; }
.temp-hot { color: var(--red); }
.temp-warm { color: var(--orange); }
.temp-cool { color: var(--green); }
.mode-badge {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
background: #21262d;
border: 1px solid var(--border);
}
#curve-svg {
width: 100%;
height: 360px;
background: #0d1117;
border-radius: 6px;
cursor: crosshair;
touch-action: none;
}
.axis-label { fill: var(--muted); font-size: 11px; }
.curve-line { stroke: var(--accent); stroke-width: 2; fill: none; }
.curve-fill { fill: rgba(88, 166, 255, 0.08); }
.curve-point {
fill: var(--accent);
stroke: #fff;
stroke-width: 2;
cursor: grab;
}
.curve-point:active { cursor: grabbing; }
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
th, td {
padding: 0.4rem 0.5rem;
text-align: left;
border-bottom: 1px solid var(--border);
}
th { color: var(--muted); font-weight: 500; }
input[type="number"] {
width: 4rem;
background: var(--bg);
border: 1px solid var(--border);
color: var(--text);
border-radius: 4px;
padding: 0.25rem 0.4rem;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-top: 1rem;
}
button {
background: #21262d;
border: 1px solid var(--border);
color: var(--text);
padding: 0.5rem 0.9rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
}
button:hover { border-color: var(--accent); }
button.primary {
background: #238636;
border-color: #2ea043;
}
button.primary:hover { background: #2ea043; }
button.danger {
background: #da3633;
border-color: #f85149;
}
#toast {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 0.75rem 1rem;
border-radius: 6px;
background: var(--panel);
border: 1px solid var(--border);
display: none;
z-index: 100;
}
#toast.error { border-color: var(--red); color: var(--red); }
#toast.ok { border-color: var(--green); }
.hint { color: var(--muted); font-size: 0.8rem; margin-top: 0.5rem; }
.monitoring-section { margin-top: 1rem; }
.gauge-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin-bottom: 1rem;
}
@media (max-width: 700px) {
.gauge-grid { grid-template-columns: 1fr; }
}
.gauge {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.75rem 1rem;
}
.gauge-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
}
.gauge-label { color: var(--muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }
.gauge-value { font-size: 1.25rem; font-weight: 600; }
.gauge-bar {
height: 10px;
background: #21262d;
border-radius: 5px;
overflow: hidden;
}
.gauge-fill {
height: 100%;
border-radius: 5px;
transition: width 0.4s ease;
}
.gauge-fill.util { background: linear-gradient(90deg, var(--green), var(--orange), var(--red)); }
.gauge-fill.power { background: linear-gradient(90deg, var(--accent), var(--orange), var(--red)); }
.gauge-fill.vram { background: linear-gradient(90deg, #6e40c9, var(--accent)); }
.gauge-sub { color: var(--muted); font-size: 0.75rem; margin-top: 0.35rem; }
.chart-wrap {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
}
.chart-legend {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-bottom: 0.5rem;
font-size: 0.8rem;
color: var(--muted);
}
.chart-legend span::before {
content: '';
display: inline-block;
width: 10px;
height: 3px;
margin-right: 0.35rem;
vertical-align: middle;
border-radius: 1px;
}
.legend-util::before { background: var(--accent); }
.legend-power::before { background: var(--orange); }
.legend-temp::before { background: var(--red); }
#history-canvas {
width: 100%;
height: 140px;
display: block;
}
.sensor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
@media (max-width: 700px) {
.sensor-grid { grid-template-columns: repeat(2, 1fr); }
}
.sensor-tile {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.75rem;
text-align: center;
}
.sensor-value {
font-size: 1.35rem;
font-weight: 600;
line-height: 1.2;
}
.sensor-label {
color: var(--muted);
font-size: 0.75rem;
margin-top: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.04em;
}
</style>
</head>
<body>
<header>
<div>
<h1>GPU Fan Control</h1>
<div class="subtitle" id="gpu-name">Ładowanie…</div>
</div>
<span class="mode-badge" id="mode-badge"></span>
</header>
<main>
<div class="grid">
<div class="card">
<h2>Krzywa wentylatorów</h2>
<svg id="curve-svg" viewBox="0 0 600 360">
<defs>
<linearGradient id="tempGrad" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="#3fb950" stop-opacity="0.15"/>
<stop offset="50%" stop-color="#d29922" stop-opacity="0.15"/>
<stop offset="100%" stop-color="#f85149" stop-opacity="0.15"/>
</linearGradient>
</defs>
<rect x="50" y="20" width="520" height="300" fill="url(#tempGrad)" stroke="#30363d"/>
<text x="300" y="355" class="axis-label" text-anchor="middle">Temperatura (°C)</text>
<text x="15" y="170" class="axis-label" transform="rotate(-90 15 170)" text-anchor="middle">Prędkość (%)</text>
<path id="curve-fill" class="curve-fill"/>
<polyline id="curve-line" class="curve-line"/>
<g id="points-layer"></g>
<line id="live-temp" stroke="#f85149" stroke-width="1" stroke-dasharray="4 4" opacity="0.7"/>
<circle id="live-dot" r="5" fill="#f85149" opacity="0.9"/>
</svg>
<p class="hint">Przeciągnij punkty na wykresie. Prędkość: 30100% (API NVIDIA). Min. 3, max. 7 punktów.</p>
<table id="points-table">
<thead>
<tr><th>#</th><th>Temp (°C)</th><th>Speed (%)</th><th></th></tr>
</thead>
<tbody></tbody>
</table>
<div class="actions">
<button type="button" id="btn-add">+ Punkt</button>
<button type="button" class="primary" id="btn-save">Zapisz krzywą</button>
<button type="button" id="btn-reload">Przeładuj z pliku</button>
<button type="button" id="btn-auto">Tryb auto (driver)</button>
<button type="button" class="danger" id="btn-max">Manual 100%</button>
</div>
</div>
<div class="card">
<h2>Status GPU</h2>
<div class="stats">
<div class="stat">
<span class="stat-label">Wentylatory</span>
<span class="stat-value" id="stat-fans"></span>
</div>
<div class="stat">
<span class="stat-label">Cel (krzywa)</span>
<span class="stat-value" id="stat-target"></span>
</div>
</div>
</div>
</div>
<div class="card monitoring-section">
<h2>Monitoring GPU</h2>
<div class="gauge-grid">
<div class="gauge">
<div class="gauge-header">
<span class="gauge-label">Obciążenie GPU</span>
<span class="gauge-value" id="gauge-util-val"></span>
</div>
<div class="gauge-bar"><div class="gauge-fill util" id="gauge-util-bar" style="width:0%"></div></div>
</div>
<div class="gauge">
<div class="gauge-header">
<span class="gauge-label">Pobór mocy</span>
<span class="gauge-value" id="gauge-power-val"></span>
</div>
<div class="gauge-bar"><div class="gauge-fill power" id="gauge-power-bar" style="width:0%"></div></div>
<div class="gauge-sub" id="gauge-power-sub"></div>
</div>
<div class="gauge">
<div class="gauge-header">
<span class="gauge-label">Pamięć VRAM</span>
<span class="gauge-value" id="gauge-vram-val"></span>
</div>
<div class="gauge-bar"><div class="gauge-fill vram" id="gauge-vram-bar" style="width:0%"></div></div>
<div class="gauge-sub" id="gauge-vram-sub"></div>
</div>
</div>
<div class="chart-wrap">
<div class="chart-legend">
<span class="legend-util">Wykorzystanie GPU (%)</span>
<span class="legend-power">Moc (W)</span>
<span class="legend-temp">Temperatura (°C)</span>
</div>
<canvas id="history-canvas"></canvas>
</div>
<div class="sensor-grid">
<div class="sensor-tile">
<div class="sensor-value" id="sensor-temp"></div>
<div class="sensor-label">Temperatura</div>
</div>
<div class="sensor-tile">
<div class="sensor-value" id="sensor-fans"></div>
<div class="sensor-label">Wentylatory</div>
</div>
<div class="sensor-tile">
<div class="sensor-value" id="sensor-gpu-clock"></div>
<div class="sensor-label">Taktowanie GPU</div>
</div>
<div class="sensor-tile">
<div class="sensor-value" id="sensor-mem-clock"></div>
<div class="sensor-label">Taktowanie pamięci</div>
</div>
<div class="sensor-tile">
<div class="sensor-value" id="sensor-mem-util"></div>
<div class="sensor-label">Wykorzystanie pamięci</div>
</div>
<div class="sensor-tile">
<div class="sensor-value" id="sensor-mode"></div>
<div class="sensor-label">Tryb wentylatora</div>
</div>
</div>
</div>
</main>
<div id="toast"></div>
<script>
const PAD = { left: 50, top: 20, width: 520, height: 300 };
const TEMP_MIN = 20;
const TEMP_MAX = 90;
const SPEED_MIN = 30;
const SPEED_MAX = 100;
let points = [];
let liveTemp = null;
let dragging = null;
const history = [];
const HISTORY_MAX = 60;
let apiKey = localStorage.getItem('gpu-fan-api-key') || '';
const urlKey = new URLSearchParams(location.search).get('api_key');
if (urlKey) {
apiKey = urlKey;
localStorage.setItem('gpu-fan-api-key', urlKey);
history.replaceState({}, '', location.pathname);
}
function headers() {
const h = { 'Content-Type': 'application/json' };
if (apiKey) h['X-API-Key'] = apiKey;
return h;
}
function toast(msg, type = 'ok') {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = type;
el.style.display = 'block';
setTimeout(() => { el.style.display = 'none'; }, 3500);
}
async function api(path, opts = {}) {
const res = await fetch(path, { ...opts, headers: { ...headers(), ...opts.headers } });
if (res.status === 401) {
const key = prompt('Podaj API key (X-API-Key):');
if (key) {
apiKey = key;
localStorage.setItem('gpu-fan-api-key', key);
return api(path, opts);
}
throw new Error('Brak autoryzacji');
}
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || res.statusText);
return data;
}
function tempToX(t) {
return PAD.left + ((t - TEMP_MIN) / (TEMP_MAX - TEMP_MIN)) * PAD.width;
}
function speedToY(s) {
return PAD.top + PAD.height - ((s - SPEED_MIN) / (SPEED_MAX - SPEED_MIN)) * PAD.height;
}
function xToTemp(x) {
return Math.round(TEMP_MIN + ((x - PAD.left) / PAD.width) * (TEMP_MAX - TEMP_MIN));
}
function yToSpeed(y) {
const raw = SPEED_MIN + ((PAD.top + PAD.height - y) / PAD.height) * (SPEED_MAX - SPEED_MIN);
return Math.round(Math.max(SPEED_MIN, Math.min(SPEED_MAX, raw)));
}
function sortPoints() {
points.sort((a, b) => a.temp - b.temp);
}
function drawCurve() {
sortPoints();
const pts = points.map(p => `${tempToX(p.temp)},${speedToY(p.speed)}`).join(' ');
document.getElementById('curve-line').setAttribute('points', pts);
const fill = points.length
? `${PAD.left},${PAD.top + PAD.height} ` + pts + ` ${PAD.left + PAD.width},${PAD.top + PAD.height}`
: '';
document.getElementById('curve-fill').setAttribute('d', fill ? `M ${fill} Z` : '');
const layer = document.getElementById('points-layer');
layer.innerHTML = '';
points.forEach((p, i) => {
const c = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
c.setAttribute('cx', tempToX(p.temp));
c.setAttribute('cy', speedToY(p.speed));
c.setAttribute('r', 8);
c.classList.add('curve-point');
c.dataset.index = i;
c.addEventListener('mousedown', e => { dragging = i; e.preventDefault(); });
c.addEventListener('touchstart', e => { dragging = i; e.preventDefault(); }, { passive: false });
layer.appendChild(c);
});
if (liveTemp != null) {
const x = tempToX(liveTemp);
document.getElementById('live-temp').setAttribute('x1', x);
document.getElementById('live-temp').setAttribute('y1', PAD.top);
document.getElementById('live-temp').setAttribute('x2', x);
document.getElementById('live-temp').setAttribute('y2', PAD.top + PAD.height);
document.getElementById('live-dot').setAttribute('cx', x);
document.getElementById('live-dot').setAttribute('cy', speedToY(interpolate(liveTemp)));
document.getElementById('live-dot').style.display = '';
} else {
document.getElementById('live-dot').style.display = 'none';
}
renderTable();
}
function interpolate(temp) {
if (!points.length) return SPEED_MIN;
if (temp <= points[0].temp) return points[0].speed;
if (temp >= points[points.length - 1].temp) return points[points.length - 1].speed;
for (let i = 0; i < points.length - 1; i++) {
const a = points[i], b = points[i + 1];
if (temp >= a.temp && temp <= b.temp) {
const r = (temp - a.temp) / (b.temp - a.temp);
return Math.round(a.speed + r * (b.speed - a.speed));
}
}
return points[points.length - 1].speed;
}
function renderTable() {
const tbody = document.querySelector('#points-table tbody');
tbody.innerHTML = '';
points.forEach((p, i) => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${i + 1}</td>
<td><input type="number" min="0" max="120" value="${p.temp}" data-i="${i}" data-field="temp"></td>
<td><input type="number" min="30" max="100" value="${p.speed}" data-i="${i}" data-field="speed"></td>
<td><button type="button" data-del="${i}" ${points.length <= 3 ? 'disabled' : ''}>×</button></td>
`;
tbody.appendChild(tr);
});
tbody.querySelectorAll('input').forEach(inp => {
inp.addEventListener('change', () => {
const i = +inp.dataset.i;
const v = +inp.value;
if (inp.dataset.field === 'temp') points[i].temp = v;
else points[i].speed = Math.max(SPEED_MIN, Math.min(SPEED_MAX, v));
drawCurve();
});
});
tbody.querySelectorAll('button[data-del]').forEach(btn => {
btn.addEventListener('click', () => {
if (points.length <= 3) return;
points.splice(+btn.dataset.del, 1);
drawCurve();
});
});
}
function onPointerMove(clientX, clientY) {
if (dragging === null) return;
const svg = document.getElementById('curve-svg');
const rect = svg.getBoundingClientRect();
const scaleX = 600 / rect.width;
const scaleY = 360 / rect.height;
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
let temp = xToTemp(x);
let speed = yToSpeed(y);
temp = Math.max(TEMP_MIN, Math.min(TEMP_MAX, temp));
const prev = dragging > 0 ? points[dragging - 1].temp + 1 : TEMP_MIN;
const next = dragging < points.length - 1 ? points[dragging + 1].temp - 1 : TEMP_MAX;
points[dragging].temp = Math.max(prev, Math.min(next, temp));
points[dragging].speed = speed;
drawCurve();
}
document.addEventListener('mousemove', e => onPointerMove(e.clientX, e.clientY));
document.addEventListener('mouseup', () => { dragging = null; });
document.addEventListener('touchmove', e => {
if (e.touches[0]) onPointerMove(e.touches[0].clientX, e.touches[0].clientY);
}, { passive: false });
document.addEventListener('touchend', () => { dragging = null; });
async function loadCurve() {
const data = await api('/api/curve');
points = data.points;
drawCurve();
}
function fmtMb(mb) {
if (mb == null) return '—';
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`;
return `${mb} MB`;
}
function tempClass(temp) {
return temp >= 70 ? 'temp-hot' : temp >= 55 ? 'temp-warm' : 'temp-cool';
}
function pushHistory(s) {
history.push({
t: Date.now(),
util: s.utilization_pct ?? 0,
power: s.power_w ?? 0,
temp: s.temperature_c ?? 0,
});
if (history.length > HISTORY_MAX) history.shift();
}
function updateGauges(s) {
const util = s.utilization_pct ?? 0;
document.getElementById('gauge-util-val').textContent = `${util}%`;
document.getElementById('gauge-util-bar').style.width = `${Math.min(100, util)}%`;
const power = s.power_w ?? 0;
const powerLimit = s.power_limit_w;
document.getElementById('gauge-power-val').textContent = `${power} W`;
const powerPct = powerLimit ? Math.min(100, (power / powerLimit) * 100) : Math.min(100, (power / 450) * 100);
document.getElementById('gauge-power-bar').style.width = `${powerPct}%`;
document.getElementById('gauge-power-sub').textContent =
powerLimit != null ? `Limit: ${powerLimit} W` : 'Limit: —';
const used = s.memory_used_mb;
const total = s.memory_total_mb;
if (used != null && total) {
const pct = Math.min(100, (used / total) * 100);
document.getElementById('gauge-vram-val').textContent = fmtMb(used);
document.getElementById('gauge-vram-bar').style.width = `${pct}%`;
document.getElementById('gauge-vram-sub').textContent = `${fmtMb(used)} / ${fmtMb(total)}`;
} else {
document.getElementById('gauge-vram-val').textContent = '—';
document.getElementById('gauge-vram-bar').style.width = '0%';
document.getElementById('gauge-vram-sub').textContent = '—';
}
}
function drawSparklines() {
const canvas = document.getElementById('history-canvas');
const ctx = canvas.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
const w = rect.width;
const h = rect.height;
const pad = { top: 8, right: 8, bottom: 8, left: 8 };
const plotW = w - pad.left - pad.right;
const plotH = h - pad.top - pad.bottom;
ctx.fillStyle = '#0d1117';
ctx.fillRect(0, 0, w, h);
if (history.length < 2) {
ctx.fillStyle = '#8b949e';
ctx.font = '12px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.fillText('Zbieranie danych…', w / 2, h / 2);
return;
}
const maxPower = Math.max(...history.map(p => p.power), 1);
const maxTemp = Math.max(...history.map(p => p.temp), 1);
function drawSeries(key, color, maxVal) {
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
const n = history.length;
history.forEach((p, i) => {
const x = pad.left + (i / Math.max(1, n - 1)) * plotW;
const y = pad.top + plotH - (p[key] / maxVal) * plotH;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
}
drawSeries('util', '#58a6ff', 100);
drawSeries('power', '#d29922', maxPower);
drawSeries('temp', '#f85149', maxTemp);
}
function updateSensorTiles(s) {
const temp = s.temperature_c;
const tempEl = document.getElementById('sensor-temp');
tempEl.textContent = temp != null ? `${temp}°C` : '—';
tempEl.className = 'sensor-value ' + (temp != null ? tempClass(temp) : '');
document.getElementById('sensor-fans').textContent =
(s.fan_speeds_pct || []).map(f => f >= 0 ? `${f}%` : '—').join(' / ') || '—';
document.getElementById('sensor-gpu-clock').textContent =
s.clock_graphics_mhz != null ? `${s.clock_graphics_mhz} MHz` : '—';
document.getElementById('sensor-mem-clock').textContent =
s.clock_memory_mhz != null ? `${s.clock_memory_mhz} MHz` : '—';
document.getElementById('sensor-mem-util').textContent =
s.memory_utilization_pct != null ? `${s.memory_utilization_pct}%` : '—';
document.getElementById('sensor-mode').textContent = s.mode || '—';
}
async function refreshStatus() {
try {
const s = await api('/api/status');
document.getElementById('gpu-name').textContent = s.gpu_name || '—';
document.getElementById('mode-badge').textContent = `Tryb: ${s.mode}`;
liveTemp = s.temperature_c;
document.getElementById('stat-fans').textContent =
(s.fan_speeds_pct || []).map(f => f >= 0 ? `${f}%` : '—').join(' / ') || '—';
document.getElementById('stat-target').textContent =
s.target_speed_pct != null ? `${s.target_speed_pct}%` : 'auto';
pushHistory(s);
updateGauges(s);
updateSensorTiles(s);
drawSparklines();
drawCurve();
} catch (e) {
console.error(e);
}
}
document.getElementById('btn-save').addEventListener('click', async () => {
try {
await api('/api/curve', { method: 'PUT', body: JSON.stringify({ points }) });
toast('Krzywa zapisana');
refreshStatus();
} catch (e) { toast(e.message, 'error'); }
});
document.getElementById('btn-reload').addEventListener('click', async () => {
try {
await api('/api/reload', { method: 'POST' });
await loadCurve();
toast('Przeładowano z pliku');
} catch (e) { toast(e.message, 'error'); }
});
document.getElementById('btn-auto').addEventListener('click', async () => {
try {
await api('/api/mode', { method: 'POST', body: JSON.stringify({ mode: 'auto' }) });
toast('Tryb auto — sterowanie driverem NVIDIA');
refreshStatus();
} catch (e) { toast(e.message, 'error'); }
});
document.getElementById('btn-max').addEventListener('click', async () => {
try {
await api('/api/mode', { method: 'POST', body: JSON.stringify({ mode: 'manual', speed: 100 }) });
toast('Manual 100%');
refreshStatus();
} catch (e) { toast(e.message, 'error'); }
});
document.getElementById('btn-add').addEventListener('click', () => {
if (points.length >= 7) { toast('Max 7 punktów', 'error'); return; }
const last = points[points.length - 1];
const temp = Math.min(TEMP_MAX - 5, (last?.temp ?? 40) + 10);
const speed = Math.min(100, (last?.speed ?? 50) + 10);
points.push({ temp, speed });
drawCurve();
});
loadCurve().catch(e => toast(e.message, 'error'));
refreshStatus();
setInterval(refreshStatus, 2000);
window.addEventListener('resize', () => { if (history.length) drawSparklines(); });
</script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
# llama.cpp stack (planned)
Placeholder for a future Docker stack serving **GGUF** models from `models.catalog.yaml` (lmstudio-community Q4).
vLLM on port **8000** stays separate; this stack is intended for port **8001**.
## Why a separate host
| Format | Runtime | Notes |
|--------|---------|-------|
| AWQ / safetensors (HF) | vLLM (`stacks/vllm/`) | Interim Q4 equivalent |
| GGUF Q4_K_M / Q4_0 | **llama.cpp** (this stack) | Native K/V cache `q4_0` like LM Studio |
Standard `vllm/vllm-openai` does **not** load `.gguf` files. Links in the catalog point here.
## Disk layout (created by vLLM scripts)
```
/data/apps/gguf/
├── qwen3.6-27b/Qwen3.6-27B-Q4_K_M.gguf
└── gemma-4-12b/
├── gemma-4-12B-it-QAT-Q4_0.gguf
└── mmproj-gemma-4-12B-it-QAT-BF16.gguf
```
Download on demand from vLLM stack:
```bash
cd stacks/vllm
./scripts/download-model.sh qwen3.6-27b-q4-gguf
./scripts/download-model.sh gemma-4-12b-q4-gguf
```
## Planned configuration
- **Binary:** `llama-server` (llama.cpp)
- **Router mode:** `--models-dir /data/apps/gguf` — multiple models on disk, one loaded per request via API `"model"` field
- **K/V cache:** `--cache-type-k q4_0 --cache-type-v q4_0` (LM Studio parity)
- **Port:** `8001` (vLLM remains on `8000`)
- **GPU:** RTX 3090 Ti only (`CUDA_VISIBLE_DEVICES=0`)
Example (not wired yet):
```bash
llama-server \
--host 0.0.0.0 \
--port 8001 \
--models-dir /data/apps/gguf \
--ctx-size 131072 \
--cache-type-k q4_0 \
--cache-type-v q4_0 \
--n-gpu-layers 999
```
## Model switching
Unlike vLLM (one model per container restart), llama.cpp router mode can keep several GGUF files on disk and select by name in the OpenAI-compatible API without restarting the whole service.
See: [Model management in llama.cpp](https://huggingface.co/blog/ggml-org/model-management-in-llamacpp)
## Status
- Catalog entries: `stacks/vllm/models.catalog.yaml` (`runtime: llamacpp`)
- Docker compose: **not implemented** — this README only
- When ready: add `docker-compose.yml`, profile, and `scripts/start.sh` mirroring the vLLM stack pattern
+16
View File
@@ -0,0 +1,16 @@
# Data disk mount point
DATA_ROOT=/data
# LocalAI web UI + OpenAI-compatible API (localhost bind when behind NPMPlus)
LOCALAI_PORT=8070
# Bearer token for /v1/* API
LOCALAI_API_KEY=
# Pinned GPU image for CUDA 13 (RTX 3090 Ti)
LOCALAI_IMAGE=localai/localai:v4.4.3-gpu-nvidia-cuda-13
# Use only the discrete NVIDIA GPU
CUDA_VISIBLE_DEVICES=0
DEBUG=false
+2
View File
@@ -0,0 +1,2 @@
.env
upstream/
+152
View File
@@ -0,0 +1,152 @@
# LocalAI stack
[LocalAI](https://github.com/mudler/LocalAI) — silnik inference z **wbudowanym UI** (chat) i API kompatybilnym z OpenAI. Obsługuje modele skwantyzowane (GGUF, AWQ, …) przez backendy (llama.cpp, vLLM, …).
## Porty
| Serwis | Port | URL |
|--------|------|-----|
| LocalAI UI + API | **8080** | `http://HOST:8080` |
| vLLM (osobny stack) | 8000 | tylko API, bez UI |
Jeden port — UI i API na tym samym endpoincie.
## Jak to działa
```mermaid
flowchart LR
browser["Przeglądarka"]
api["curl / OpenAI SDK"]
localai["LocalAI :8080"]
gpu["RTX 3090 Ti"]
models["/data/apps/localai/models"]
browser --> localai
api --> localai
localai --> gpu
localai --> models
```
| Element | Opis |
|---------|------|
| Obraz | `localai/localai:v4.4.3-gpu-nvidia-cuda-13` |
| Konfiguracja | `.env` + `docker-compose.yml` |
| Modele | `/data/apps/localai/models` (puste na start) |
| Upstream repo | opcjonalnie `upstream/` przez `clone-upstream.sh` |
## Struktura
```
stacks/localai/
├── README.md
├── docker-compose.yml
├── .env.example
├── .gitignore
├── coding-agent/ # notatki dla agenta (KV cache, STATE)
├── profiles/ # szablony YAML (KV q8_0)
├── upstream/ # shallow clone (gitignored)
└── scripts/
├── clone-upstream.sh
├── pull.sh
├── start.sh
└── apply-kv-profile.sh
```
Na dysku `/data`:
```
/data/apps/localai/
├── models/ # GGUF, YAML model configs
├── backends/ # custom backends
├── configuration/ # api_keys.json, runtime settings
├── images/ # generated images
└── data/ # agents, skills, persistent app data
```
## Workflow (bez modelu)
```bash
cd stacks/localai
cp .env.example .env
# opcjonalnie — referencja YAML z GitHub
./scripts/clone-upstream.sh
# tylko obraz Docker
./scripts/pull.sh
# uruchom (pusty katalog models/)
./scripts/start.sh
```
Weryfikacja:
```bash
curl -s http://localhost:8080/readyz
# UI: http://<IP-serwera>:8080
```
## Zmienne `.env`
| Zmienna | Opis | Domyślnie |
|---------|------|-----------|
| `DATA_ROOT` | Mount dysku danych | `/data` |
| `LOCALAI_PORT` | Port na hoście | `8080` |
| `LOCALAI_IMAGE` | Obraz Docker (CUDA 13) | `localai/localai:v4.4.3-gpu-nvidia-cuda-13` |
| `CUDA_VISIBLE_DEVICES` | GPU | `0` |
| `DEBUG` | Verbose logs | `false` |
## VRAM (24 GB)
Compose ustawia `SINGLE_ACTIVE_BACKEND=true` i `PARALLEL_REQUESTS=false` — jeden aktywny backend/model naraz.
**Nie uruchamiaj** dużego modelu w vLLM i LocalAI równocześnie na tej samej karcie:
```bash
cd ../vllm && docker compose --profile vllm down
```
## Modele (później)
- UI → Model Gallery w przeglądarce
- CLI w kontenerze: `docker exec -it localai local-ai models install ...`
- Ręcznie: GGUF + YAML w `/data/apps/localai/models/`
GGUF z [`stacks/vllm/models.catalog.yaml`](../vllm/models.catalog.yaml) można skopiować lub podlinkować do `models/`.
## KV cache (skwantyzowany q8_0)
Domyślnie llama.cpp trzyma KV cache w **f16** — dużo VRAM przy długim kontekście. Ustawienia są **per model** w YAML na `/data`, nie w compose.
| Pole | Rekomendacja |
|------|--------------|
| `cache_type_k` | `q8_0` |
| `cache_type_v` | `q8_0` |
| `flash_attention` | `true` (wymagane przy q8_0 V) |
| `context_size` | `8192` (start; zwiększ po teście VRAM) |
Szablon: [`profiles/gemma-4-12b-q4-kv-q8.yaml.example`](profiles/gemma-4-12b-q4-kv-q8.yaml.example)
Zastosowanie na istniejącym modelu:
```bash
./scripts/apply-kv-profile.sh gemma-4-12b-it-qat-q4_0
docker compose --profile localai restart localai
```
Szczegóły: [`coding-agent/KV-CACHE.md`](coding-agent/KV-CACHE.md)
## Zarządzanie
```bash
docker compose --profile localai ps
docker compose --profile localai logs -f localai
docker compose --profile localai restart localai
docker compose --profile localai down
```
## Dokumentacja
Tutorial: [manual-tutorial/05-localai-stack.md](../../manual-tutorial/05-localai-stack.md)
Upstream: [github.com/mudler/LocalAI](https://github.com/mudler/LocalAI)
+28
View File
@@ -0,0 +1,28 @@
# BACKLOG — LocalAI stack
## P0 — KV cache (bieżąca sesja)
- [x] Audyt konfiguracji KV (compose, YAML, backendy)
- [x] Szablon `profiles/gemma-4-12b-q4-kv-q8.yaml.example`
- [x] Skrypt `scripts/apply-kv-profile.sh`
- [x] Aktualizacja YAML Gemma na `/data` (via `docker exec` — plik root-owned)
- [x] Restart `localai` — health OK
- [ ] **Test VRAM po załadowaniu Gemma GGUF** — uzupełnić STATE.md
## P1 — po pierwszym modelu chat
- [ ] Dostroić `context_size` (8192 → 16384 jeśli VRAM pozwala)
- [ ] Porównanie jakości odpowiedzi f16 vs q8_0 KV (krótki prompt)
- [ ] Profil KV dla Qwen3.6-27B GGUF (gdy dodany do LocalAI)
- [ ] Przekazać `LOCALAI_API_KEY` do `docker-compose.yml` (zsynchronizować z root BACKLOG)
## P2 — opcjonalnie
- [ ] Backend `turboquant` + `turbo3`/`turbo4` (~34× kompresja KV)
- [ ] Skrypt sync GGUF z `stacks/vllm/models.catalog.yaml`
- [ ] Reverse proxy + firewall (root tutorial 07)
## P3 — dokumentacja
- [ ] Zsynchronizować port 8070 vs 8080 w całym repo (root BACKLOG)
- [ ] Przykład `curl /v1/chat/completions` z auth w tutorialu
@@ -0,0 +1,41 @@
# Konwencje — stack LocalAI
Skrót reguł specyficznych dla tego stacku. Pełne konwencje repo: [`../../coding-agent/CONVENTIONS.md`](../../coding-agent/CONVENTIONS.md).
## Ścieżki
| Warstwa | Ścieżka |
|---------|---------|
| Repo stack | `ubuntu-bare-metal/stacks/localai/` |
| `.env` (sekrety) | `stacks/localai/.env`**gitignore** |
| Modele runtime | `/data/apps/localai/models/` |
| YAML modeli | `/data/apps/localai/models/*.yaml`**poza git** |
| Szablony KV | `stacks/localai/profiles/*.yaml.example` |
| Notatki agenta | `stacks/localai/coding-agent/` |
## KV cache
- Ustawienia **tylko** w sekcji `parameters:` pliku YAML modelu.
- Skwantyzowany `cache_type_v` wymaga `flash_attention: true`.
- Dozwolone na `llama-cpp`: `f16`, `f32`, `q8_0`, `q4_0`, `q4_1`, `q5_0`, `q5_1`.
- Startowa rekomendacja: `cache_type_k: q8_0`, `cache_type_v: q8_0`, `context_size: 8192`.
- Modele embedding (np. bge-m3) — **nie** dodawać KV cache.
## Docker
```bash
cd stacks/localai
docker compose --profile localai up -d
docker compose --profile localai restart localai
```
Port wewnątrz kontenera zawsze **8080**; host mapuje `LOCALAI_PORT` (użytkownik: **8070**).
## VRAM
- Jeden duży model chat na GPU naraz.
- Przed loadem dużego modelu: `cd ../vllm && docker compose --profile vllm down`.
## Sekrety
- `LOCALAI_API_KEY` — tylko w `.env` na serwerze, nie w `coding-agent/`.
@@ -0,0 +1,147 @@
# RTX1 — raport naprawy embeddingu (odpowiedź dla ai-lawyer-srvr)
**Typ dokumentu:** raport po stronie **RTX1** (LocalAI na `gmktec-k11`) — odpowiedź na żądanie z hosta dev.
**Data:** 2026-06-30
**Status:** **NAPRAWIONE**`POST /v1/embeddings` zwraca HTTP 200, wektor **1024** wymiarów.
**Adresat:** agent kodujący na hoście dev (`ai-lawyer-srvr`).
---
## 1. Podsumowanie wykonawcze
| Obszar | Stan |
|--------|------|
| Connectivity (`llm.rtx1.mobile.agency-ai.dev`) | OK (wcześniej potwierdzone przez dev) |
| Graph LLM (`gemma-4-12b-it-qat-q4_0`) | OK |
| Embedding (`bge-m3-FP16.gguf`) | **OK** po poprawce YAML |
| Zmiana `.env` dev | **Nie wymagana** |
---
## 2. Root cause
Model `bge-m3-FP16.gguf` został zaimportowany z galerii LocalAI z konfiguracją **chat**, bez flagi embedding:
```yaml
# PRZED (błędne)
known_usecases:
- chat
# brak: embeddings: true
```
Skutek: worker llama-cpp padał przy `POST /v1/embeddings` z błędem:
```
rpc error: code = Unavailable desc = error reading from server: EOF
```
**Nie było to:** zły URL, API key, brak pliku GGUF (1.1 GB OK), ani brak VRAM w stanie idle.
---
## 3. Zastosowana poprawka
Zaktualizowano `/data/apps/localai/models/bge-m3-FP16.gguf.yaml`:
```yaml
name: bge-m3-FP16.gguf
backend: llama-cpp
embeddings: true
description: BGE-M3 embedding model (1024 dims)
known_usecases:
- embedding
parameters:
model: bge-m3-FP16.gguf
context_size: 8192
```
Szablon w repo (na przyszłe reimporty): [`../profiles/bge-m3-FP16-embedding.yaml.example`](../profiles/bge-m3-FP16-embedding.yaml.example)
Restart: `docker compose --profile localai restart localai`
---
## 4. Wyniki testów akceptacyjnych (RTX1)
| Test | Endpoint | HTTP | Wynik |
|------|----------|------|-------|
| Embeddings lokalnie | `POST http://127.0.0.1:8070/v1/embeddings` | **200** | wektor **1024** dims |
| Chat → embed (swap) | gemma chat, potem bge embed | **200** / **200** | `SINGLE_ACTIVE_BACKEND` OK |
| Embeddings przez domenę z RTX1 | `POST https://llm.rtx1.mobile.agency-ai.dev/v1/embeddings` | timeout (000) | hairpin NAT z wnętrza LAN — **nie blokuje dev** |
Test lokalny (na RTX1):
```bash
curl -s http://127.0.0.1:8070/v1/embeddings \
-H "Authorization: Bearer $LOCALAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"test prawny","model":"bge-m3-FP16.gguf"}' \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d['data'][0]['embedding']))"
# Wynik: 1024
```
---
## 5. Wartości dla `.env` dev — bez zmian
```bash
EMBEDDING_URL=https://llm.rtx1.mobile.agency-ai.dev/v1
GRAPH_LLM_URL=https://llm.rtx1.mobile.agency-ai.dev/v1
EMBEDDING_MODEL=bge-m3-FP16.gguf
GRAPH_LLM_MODEL=gemma-4-12b-it-qat-q4_0
EMBEDDING_DIMS=1024
```
`EMBEDDING_API_KEY` / `GRAPH_LLM_API_KEY` — bez zmian (ten sam token co `LOCALAI_API_KEY` na RTX1).
---
## 6. Checklist dla agenta dev (wykonaj teraz)
1. [ ] `docker compose -f docker-compose.dev.yml exec api python scripts/discover_rtx1_models.py`
Oczekiwane: embed test **OK**, chat test **OK**
2. [ ] `docker compose -f docker-compose.dev.yml up -d api` (jeśli zmieniano `.env`)
3. [ ] Smoke hybrid chat — SSE z `"route": "hybrid"` i semantycznym retrieval
4. [ ] Jeśli ES był indeksowany bez wektorów: `python scripts/reindex_embeddings.py`
Test z hosta dev:
```bash
curl -s https://llm.rtx1.mobile.agency-ai.dev/v1/embeddings \
-H "Authorization: Bearer $EMBEDDING_API_KEY" \
-H "Content-Type: application/json" \
-d '{"input":"test prawny","model":"bge-m3-FP16.gguf"}' \
| python3 -c "import sys,json; d=json.load(sys.stdin); print('dims:', len(d['data'][0]['embedding']))"
```
Oczekiwane: `dims: 1024`
---
## 7. Architektura (bez zmian)
- Publiczny URL: `https://llm.rtx1.mobile.agency-ai.dev/v1`
- LocalAI LAN: `http://192.168.100.5:8070` (bind `0.0.0.0`)
- `SINGLE_ACTIVE_BACKEND=true` — gemma i bge przełączają się sekwencyjnie (OK dla hybrid RAG: embed RTX1 → chat RTX2)
---
## 8. Mapa dokumentacji
| Plik | Opis |
|------|------|
| Ten dokument | Status naprawy embeddingu |
| [`STATE.md`](STATE.md) | Runtime LocalAI na RTX1 |
| [`../profiles/bge-m3-FP16-embedding.yaml.example`](../profiles/bge-m3-FP16-embedding.yaml.example) | Szablon YAML embedding |
---
## 9. Odpowiedź na żądanie (tabela)
| Punkt | Odpowiedź |
|-------|-----------|
| 1. Root cause | Błędny YAML — `chat` zamiast `embedding`, brak `embeddings: true` |
| 2. Poprawka | YAML + restart LocalAI |
| 3. curl embed | HTTP **200**, **1024** dims (lokalnie na RTX1) |
| 4. Zmiana `id` / dims | **Nie**`bge-m3-FP16.gguf`, `1024` |
+46
View File
@@ -0,0 +1,46 @@
# HANDOFF — LocalAI KV cache
## Cel sesji
Przeanalizować konfigurację KV cache w LocalAI i włączyć **skwantyzowany KV** (`q8_0`), aby zmieścić większe modele / dłuższy kontekst na RTX 3090 Ti (24 GB).
## Wynik audytu
1. W [`docker-compose.yml`](../docker-compose.yml) i [`.env`](../.env) **brak** ustawień KV — to prawidłowe; LocalAI konfiguruje KV w YAML modelu.
2. YAML `gemma-4-12b-it-qat-q4_0` na `/data` nie miał `cache_type_k`, `cache_type_v`, `flash_attention`, `context_size` → domyślnie **f16** KV (więcej VRAM).
3. Backend: tylko `cuda13-llama-cpp`. TurboQuant **nie** instalowany.
4. Plik GGUF Gemma **jeszcze nie pobrany** — YAML gotowy przed pierwszym loadem.
## Decyzja
| Opcja | Wybór |
|-------|-------|
| Standard `q8_0` + `flash_attention` na `llama-cpp` | **TAK** |
| Backend `turboquant` (turbo3/4) | **NIE** (odłożone) |
Uzasadnienie: ~2× mniej pamięci KV vs f16, bez nowego backendu, minimalny wpływ na jakość.
## Co zrobiono w repo
- Katalog `coding-agent/` (ten handoff + STATE, BACKLOG, KV-CACHE, CONVENTIONS)
- [`profiles/gemma-4-12b-q4-kv-q8.yaml.example`](../profiles/gemma-4-12b-q4-kv-q8.yaml.example)
- [`scripts/apply-kv-profile.sh`](../scripts/apply-kv-profile.sh)
- Sekcja KV w README stacku i tutorialu 05
## Co zrobiono na serwerze
- Zaktualizowano `/data/apps/localai/models/gemma-4-12b-it-qat-q4_0.yaml` (parametry KV)
- Restart kontenera `localai`
## Następne kroki (dla agenta / użytkownika)
1. Dokończyć pobieranie GGUF Gemma 4 12B (galeria UI lub URI z `._gallery_*.yaml`).
2. Po loadzie: `nvidia-smi` + krótki chat — zapisać VRAM w [STATE.md](STATE.md).
3. Jeśli zapas VRAM: podnieść `context_size` do 16384 w YAML.
4. Dla przyszłego Qwen3.6-27B GGUF: skopiować wzorzec KV z `profiles/`.
5. BACKLOG P0 root repo: przekazać `LOCALAI_API_KEY` do compose (osobne zadanie).
## Dokumentacja zewnętrzna
- [LocalAI model configuration — cache_type_k/v](https://localai.io/advanced/model-configuration/)
- [Text generation — llama-cpp backend](https://localai.io/features/text-generation/)
+84
View File
@@ -0,0 +1,84 @@
# KV cache w LocalAI (llama-cpp)
## Problem
Przy inference LLM pamięć KV rośnie z długością kontekstu. Domyślnie LocalAI/llama.cpp używa **f16** dla K i V — pełna precyzja, najwięcej VRAM.
Na RTX 3090 Ti (24 GB) przy modelu Q4 + mmproj (Gemma 4 12B) kwantyzacja KV zwalnia miejsce na dłuższy `context_size` lub większy model.
## Gdzie konfigurować
| Miejsce | KV cache? |
|---------|-----------|
| `docker-compose.yml` | nie |
| `.env` stacku | nie |
| `models/<nazwa>.yaml``parameters:` | **tak** |
## Pola YAML (llama-cpp)
| Pole | Typ | Domyślnie | Opis |
|------|-----|-----------|------|
| `cache_type_k` | string | `f16` | Kwantyzacja cache kluczy (`-ctk` w llama.cpp) |
| `cache_type_v` | string | `f16` | Kwantyzacja cache wartości (`-ctv`) |
| `flash_attention` | bool/string | off | **Wymagane** przy skwantyzowanym `cache_type_v` |
| `context_size` | int | niski / auto | Maks. tokenów kontekstu (wpływa na rozmiar KV) |
### Dozwolone typy (`cuda13-llama-cpp`)
`f16`, `f32`, `q8_0`, `q4_0`, `q4_1`, `q5_0`, `q5_1`
### Rekomendacja dla tego serwera (q8_0)
```yaml
parameters:
cache_type_k: q8_0
cache_type_v: q8_0
flash_attention: true
context_size: 8192
```
Po teście VRAM można podnieść `context_size` do `16384`.
## Szacunek VRAM (Gemma 4 12B Q4_0 + mmproj)
| Składnik | Orientacyjnie |
|----------|---------------|
| Wagi + mmproj | ~810 GB |
| KV @ f16, ctx 8k | ~24 GB |
| KV @ q8_0, ctx 8k | ~12 GB |
## TurboQuant (odłożone)
Backend `turboquant` + typy `turbo2`/`turbo3`/`turbo4` dają większą kompresję (~34×), ale wymagają:
```bash
docker exec localai /local-ai backends install turboquant
```
oraz `backend: turboquant` w YAML. Nie wdrożone w bieżącej sesji.
## Zastosowanie profilu
```bash
cd stacks/localai
./scripts/apply-kv-profile.sh gemma-4-12b-it-qat-q4_0
docker compose --profile localai restart localai
```
## Weryfikacja
```bash
# modele
curl -s http://localhost:8070/v1/models -H "Authorization: Bearer $KEY"
# VRAM
nvidia-smi
# logi backendu
docker compose --profile localai logs localai 2>&1 | grep -iE 'cache|ctk|ctv|flash' | tail -20
```
## Źródła
- https://localai.io/advanced/model-configuration/
- https://localai.io/features/text-generation/
+25
View File
@@ -0,0 +1,25 @@
# coding-agent — notatki dla agenta (stack LocalAI)
Katalog handoff dla sesji Cursor pracujących nad [`stacks/localai/`](../) na serwerze GMKtec K11.
## Kolejność czytania
1. **[HANDOFF.md](HANDOFF.md)** — decyzje (KV q8_0), audyt, następne kroki
2. **[STATE.md](STATE.md)** — stan runtime: kontener, modele, backendy, VRAM
3. **[KV-CACHE.md](KV-CACHE.md)** — referencja techniczna KV cache w YAML
4. **[BACKLOG.md](BACKLOG.md)** — priorytetyzowane zadania
5. **[CONVENTIONS.md](CONVENTIONS.md)** — ścieżki, sekrety, konwencje stacku
Wspólne konwencje repo: [`../../coding-agent/CONVENTIONS.md`](../../coding-agent/CONVENTIONS.md)
## Zasady
- Instrukcje dla użytkownika: **po polsku**. Komendy: **po angielsku**.
- **Nie commituj** ani nie zapisuj tutaj wartości `LOCALAI_API_KEY`, tokenów HF itd.
- YAML modeli na `/data/apps/localai/models/` **nie są w git** — szablony trzymaj w [`../profiles/`](../profiles/).
- Commity i push **tylko na prośbę** użytkownika.
- Nie edytuj plików planu w `.cursor/plans/`.
## Ostatnia aktualizacja
Sesja: audyt KV cache → wdrożenie `cache_type_k/v: q8_0` + `flash_attention` dla modeli chat (llama-cpp).
+62
View File
@@ -0,0 +1,62 @@
# STATE — LocalAI runtime
Ostatnia aktualizacja: po wdrożeniu BGE-Reranker-v2-m3 (2026-07-01).
## Kontener
| Element | Wartość |
|---------|---------|
| Nazwa | `localai` |
| Obraz | `localai/localai:v4.4.3-gpu-nvidia-cuda-13` |
| Status | running (healthy) |
| Port hosta | **8070** → 8080 w kontenerze (`0.0.0.0` — LAN) |
| GPU | `CUDA_VISIBLE_DEVICES=0` (RTX 3090 Ti) |
| API auth | `LOCALAI_API_KEY` w compose — 401 bez Bearer |
## Publiczny endpoint (NPMPlus)
| Element | Wartość |
|---------|---------|
| Domena | `https://llm.rtx1.mobile.agency-ai.dev/v1` |
| Upstream | `http://127.0.0.1:8070` (lub LAN `192.168.100.5:8070`) |
## Backendy
| Backend | Zainstalowany |
|---------|---------------|
| `cuda13-llama-cpp` (alias `llama-cpp`) | tak |
| `turboquant` | **nie** (odłożone) |
## Modele (`/data/apps/localai/models/`)
| Model | GGUF | YAML | Status API |
|-------|------|------|------------|
| `gemma-4-12b-it-qat-q4_0` | tak (~6.5 GB) | KV `q8_0`, `flash_attention`, `context_size: 8192` | chat **OK** |
| `bge-m3-FP16.gguf` | tak (1.1 GB) | `embeddings: true`, `known_usecases: [embedding]` | embed **OK**, 1024 dims |
| `bge-reranker-v2-m3-FP16.gguf` | tak (1.1 GB) | `reranking: true`, `known_usecases: [rerank]`, `backend: llama-cpp` | rerank **OK** |
Szablony w repo:
- Gemma KV: [`../profiles/gemma-4-12b-q4-kv-q8.yaml.example`](../profiles/gemma-4-12b-q4-kv-q8.yaml.example)
- BGE embed: [`../profiles/bge-m3-FP16-embedding.yaml.example`](../profiles/bge-m3-FP16-embedding.yaml.example)
- BGE rerank: [`../profiles/bge-reranker-v2-m3-FP16-rerank.yaml.example`](../profiles/bge-reranker-v2-m3-FP16-rerank.yaml.example)
## Weryfikacja (2026-06-30)
| Test | Wynik |
|------|-------|
| `GET /readyz` | 200 |
| `GET /v1/models` (auth) | 200 — 2 modele |
| `POST /v1/chat/completions` (gemma) | 200 |
| `POST /v1/embeddings` (bge-m3) | **200**, wektor **1024** |
| `POST /v1/rerank` (bge-reranker) | **200**, indeks 2 (panda) na top |
| Chat → embed (SINGLE_ACTIVE_BACKEND) | 200 / 200 |
| Embeddings z RTX1 przez publiczną domenę | timeout (hairpin NAT) — dev powinien testować z zewnątrz |
## Ścieżki
| Co | Gdzie |
|----|-------|
| Repo stack | `/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal/stacks/localai` |
| Modele runtime | `/data/apps/localai/models` |
| Raport dla dev | [`EMBEDDING-STATUS-REPORT.md`](EMBEDDING-STATUS-REPORT.md) |
+1
View File
@@ -0,0 +1 @@
docker-compose.yml
+31
View File
@@ -0,0 +1,31 @@
services:
localai:
image: ${LOCALAI_IMAGE:-localai/localai:v4.4.3-gpu-nvidia-cuda-13}
container_name: localai
profiles:
- localai
restart: unless-stopped
init: true
ports:
- "${LOCALAI_PORT:-8080}:8080"
environment:
- LOCALAI_API_KEY=${LOCALAI_API_KEY:-}
- CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-0}
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
- MODELS_PATH=/models
- DEBUG=${DEBUG:-false}
- SINGLE_ACTIVE_BACKEND=true
- PARALLEL_REQUESTS=false
volumes:
- ${DATA_ROOT:-/data}/apps/localai/models:/models
- ${DATA_ROOT:-/data}/apps/localai/backends:/backends
- ${DATA_ROOT:-/data}/apps/localai/configuration:/configuration
- ${DATA_ROOT:-/data}/apps/localai/images:/tmp/generated/images
- ${DATA_ROOT:-/data}/apps/localai/data:/data
gpus: all
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/readyz"]
interval: 1m
timeout: 20m
retries: 5
start_period: 2m
@@ -0,0 +1,16 @@
# BGE-M3 embedding model for LocalAI (llama-cpp backend).
# Copy to /data/apps/localai/models/bge-m3-FP16.gguf.yaml after gallery import.
#
# GGUF: bge-m3-FP16.gguf (~1.1 GB) in same directory as this YAML.
# API id: bge-m3-FP16.gguf
# Vector dimensions: 1024
name: bge-m3-FP16.gguf
backend: llama-cpp
embeddings: true
description: BGE-M3 embedding model (1024 dims)
known_usecases:
- embedding
parameters:
model: bge-m3-FP16.gguf
context_size: 8192
@@ -0,0 +1,27 @@
# BGE-Reranker-v2-m3 for LocalAI (llama-cpp / cuda13-llama-cpp backend).
# Copy to /data/apps/localai/models/bge-reranker-v2-m3-FP16.gguf.yaml after GGUF download.
#
# GGUF: bge-reranker-v2-m3-FP16.gguf (~1.1 GB) in same directory as this YAML.
# API id: bge-reranker-v2-m3-FP16.gguf
# Endpoint: POST /v1/rerank
#
# Do NOT use backend: rerankers for GGUF — that backend is for HuggingFace transformers.
# cuda13-llama-cpp is selected automatically on the cuda-13 image when backend: llama-cpp.
name: bge-reranker-v2-m3-FP16.gguf
backend: llama-cpp
reranking: true
embeddings: false
description: BGE-Reranker-v2-m3 cross-encoder (FP16 GGUF)
known_usecases:
- rerank
parameters:
model: bge-reranker-v2-m3-FP16.gguf
context_size: 8192
template:
use_tokenizer_template: true
function:
grammar:
disable: true
options:
- use_jinja:true
@@ -0,0 +1,32 @@
# Szablon: Gemma 4 12B Q4_0 z kwantyzowanym KV cache (q8_0)
#
# Zastosowanie na serwerze:
# ./scripts/apply-kv-profile.sh gemma-4-12b-it-qat-q4_0
# lub skopiuj sekcję parameters do /data/apps/localai/models/<nazwa>.yaml
#
# Dokumentacja: coding-agent/KV-CACHE.md
name: gemma-4-12b-it-qat-q4_0
backend: llama-cpp
mmproj: llama-cpp/mmproj/gemma-4-12B-it-qat-q4_0-gguf/mmproj-gemma-4-12b-it-qat-q4_0.gguf
known_usecases:
- chat
options:
- use_jinja:true
parameters:
model: llama-cpp/models/gemma-4-12B-it-qat-q4_0-gguf/gemma-4-12b-it-qat-q4_0.gguf
cache_type_k: q8_0
cache_type_v: q8_0
flash_attention: true
context_size: 8192
temperature: 1
top_p: 0.95
top_k: 64
repeat_penalty: 1
min_p: 0
template:
use_tokenizer_template: true
function:
automatic_tool_parsing_fallback: true
grammar:
disable: true
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Merge KV cache settings (q8_0) into a model YAML on /data.
# Usage: ./scripts/apply-kv-profile.sh <model-name-without-.yaml>
# Example: ./scripts/apply-kv-profile.sh gemma-4-12b-it-qat-q4_0
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
if [[ ! -f "${STACK_DIR}/.env" ]]; then
echo "ERROR: ${STACK_DIR}/.env not found"
exit 1
fi
set -a
# shellcheck disable=SC1091
source "${STACK_DIR}/.env"
set +a
DATA_ROOT="${DATA_ROOT:-/data}"
MODELS_DIR="${DATA_ROOT}/apps/localai/models"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <model-name>"
echo "Example: $0 gemma-4-12b-it-qat-q4_0"
exit 1
fi
MODEL_NAME="$1"
TARGET="${MODELS_DIR}/${MODEL_NAME}.yaml"
if [[ ! -f "${TARGET}" ]]; then
echo "ERROR: ${TARGET} not found"
exit 1
fi
BACKUP="${TARGET}.bak.$(date +%Y%m%d%H%M%S)"
cp "${TARGET}" "${BACKUP}"
echo "Backup: ${BACKUP}"
run_python_merge() {
python3 - "${1}" <<'PY'
import sys
from pathlib import Path
try:
import yaml
except ImportError:
sys.exit("ERROR: python3-yaml required (sudo apt install python3-yaml)")
path = Path(sys.argv[1])
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
params = data.setdefault("parameters", {})
kv = {
"cache_type_k": "q8_0",
"cache_type_v": "q8_0",
"flash_attention": True,
}
if "context_size" not in params:
kv["context_size"] = 8192
params.update(kv)
path.write_text(
yaml.dump(data, default_flow_style=False, allow_unicode=True, sort_keys=False),
encoding="utf-8",
)
print(f"Updated KV settings in {path}")
for k, v in kv.items():
print(f" {k}: {v}")
if "context_size" in params and "context_size" not in kv:
print(f" context_size: {params['context_size']} (unchanged)")
PY
}
if run_python_merge "${TARGET}" 2>/dev/null; then
:
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -qx localai; then
echo "Host write failed — applying via docker exec localai (models volume is root-owned)"
CONTAINER_PATH="/models/${MODEL_NAME}.yaml"
docker exec localai sh -c "grep -q cache_type_k '${CONTAINER_PATH}' || sed -i '/^ model:/a\\
cache_type_k: q8_0\\
cache_type_v: q8_0\\
flash_attention: true\\
context_size: 8192' '${CONTAINER_PATH}'"
grep -E 'cache_type_k|cache_type_v|flash_attention|context_size' "${TARGET}" || true
else
echo "ERROR: cannot write ${TARGET} (permission denied) and container localai not running"
echo "Run: sudo $0 ${MODEL_NAME}"
exit 1
fi
echo ""
echo "Restart LocalAI to apply:"
echo " cd ${STACK_DIR} && docker compose --profile localai restart localai"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
UPSTREAM_DIR="${STACK_DIR}/upstream"
REPO_URL="https://github.com/mudler/LocalAI.git"
TAG="v4.4.3"
cd "${STACK_DIR}"
if [[ -d "${UPSTREAM_DIR}/.git" ]]; then
echo "Upstream already cloned: ${UPSTREAM_DIR}"
echo "Remove it first to re-clone: rm -rf upstream"
exit 0
fi
echo "=== Cloning LocalAI upstream (reference only) ==="
echo "Repo: ${REPO_URL}"
echo "Tag: ${TAG}"
echo "Dest: ${UPSTREAM_DIR}"
echo ""
git clone --depth 1 --branch "${TAG}" "${REPO_URL}" "${UPSTREAM_DIR}"
echo ""
echo "Done. Use upstream/ for example model YAML and compose reference."
echo "Runtime uses the official Docker image from .env — not a local build."
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
# Download BGE-Reranker-v2-m3 FP16 GGUF and apply YAML profile.
# Usage: ./scripts/download-reranker.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${STACK_DIR}"
if [[ -f .env ]]; then
set -a
# shellcheck disable=SC1091
source .env
set +a
fi
DATA_ROOT="${DATA_ROOT:-/data}"
MODELS_DIR="${DATA_ROOT}/apps/localai/models"
GGUF_NAME="bge-reranker-v2-m3-FP16.gguf"
GGUF_URL="https://huggingface.co/gpustack/bge-reranker-v2-m3-GGUF/resolve/main/${GGUF_NAME}"
YAML_SRC="${STACK_DIR}/profiles/bge-reranker-v2-m3-FP16-rerank.yaml.example"
YAML_DST="${MODELS_DIR}/${GGUF_NAME}.yaml"
"${SCRIPT_DIR}/ensure-dirs.sh" "${DATA_ROOT}"
echo "=== BGE-Reranker-v2-m3 download ==="
echo "Target: ${MODELS_DIR}/${GGUF_NAME}"
echo ""
if [[ -f "${MODELS_DIR}/${GGUF_NAME}" ]]; then
echo "GGUF already exists — skipping download"
else
if command -v wget &>/dev/null; then
wget -c -O "${MODELS_DIR}/${GGUF_NAME}.partial" "${GGUF_URL}"
mv "${MODELS_DIR}/${GGUF_NAME}.partial" "${MODELS_DIR}/${GGUF_NAME}"
else
curl -fL -C - -o "${MODELS_DIR}/${GGUF_NAME}.partial" "${GGUF_URL}"
mv "${MODELS_DIR}/${GGUF_NAME}.partial" "${MODELS_DIR}/${GGUF_NAME}"
fi
fi
echo ""
echo "=== Applying YAML profile ==="
if cp "${YAML_SRC}" "${YAML_DST}" 2>/dev/null; then
chmod 644 "${YAML_DST}" "${MODELS_DIR}/${GGUF_NAME}" 2>/dev/null || true
elif docker ps --format '{{.Names}}' | grep -qx localai; then
echo "Host copy failed (permissions) — writing via docker exec localai"
docker exec -i localai sh -c "cat > /models/${GGUF_NAME}.yaml" < "${YAML_SRC}"
else
echo "ERROR: cannot write ${YAML_DST} (permission denied) and localai container not running"
exit 1
fi
ls -lh "${MODELS_DIR}/${GGUF_NAME}" "${YAML_DST}"
echo ""
echo "=== Done ==="
echo "Restart LocalAI to load the model:"
echo " cd ${STACK_DIR} && docker compose --profile localai restart localai"
echo ""
echo "Smoke test:"
echo ' curl -s http://127.0.0.1:${LOCALAI_PORT:-8070}/v1/rerank \'
echo ' -H "Authorization: Bearer <LOCALAI_API_KEY>" \'
echo ' -H "Content-Type: application/json" \'
echo ' -d '"'"'{"model":"bge-reranker-v2-m3-FP16.gguf","query":"panda","documents":["hi","it is a bear","The giant panda is a bear species endemic to China."],"top_n":2}'"'"
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Create LocalAI data directories on the data disk.
ensure_localai_dirs() {
local data_root="${1:-/data}"
mkdir -p \
"${data_root}/apps/localai/models" \
"${data_root}/apps/localai/backends" \
"${data_root}/apps/localai/configuration" \
"${data_root}/apps/localai/images" \
"${data_root}/apps/localai/data"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
ensure_localai_dirs "${1:-/data}"
fi

Some files were not shown because too many files have changed in this diff Show More