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:
+19
@@ -0,0 +1,19 @@
|
||||
# Secrets
|
||||
.env
|
||||
**/.env
|
||||
|
||||
# Python
|
||||
.venv/
|
||||
**/.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# Local clones / runtime
|
||||
upstream/
|
||||
*.pem
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
*.swp
|
||||
.cursor/
|
||||
@@ -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)
|
||||
@@ -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 00–03b, 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
|
||||
@@ -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 ~8–12 GB; Flux ~12–20 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: ~5–15 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
|
||||
@@ -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)
|
||||
@@ -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.*`).
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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 (00–10, 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.*
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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+)
|
||||
@@ -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 (P0–P3)
|
||||
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).
|
||||
@@ -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).
|
||||
@@ -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 161–164)
|
||||
if os.geteuid() != 0 and not DRY_RUN:
|
||||
log.error("GPU fan control requires root (NVML write access). Run with sudo.")
|
||||
sys.exit(1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Analiza coupling gpu-fan (UI + daemon)
|
||||
|
||||
### Obecna struktura procesu
|
||||
|
||||
```
|
||||
gpu-fan.service (root)
|
||||
└── python app.py
|
||||
├── FanController.run_loop() # wątek daemon — co 2s NVML
|
||||
└── uvicorn :8090
|
||||
├── GET / → static/index.html (~750 linii JS)
|
||||
├── GET /api/status
|
||||
├── GET/PUT /api/curve
|
||||
├── POST /api/mode
|
||||
└── POST /api/reload
|
||||
```
|
||||
|
||||
Frontend (`static/index.html`) używa **względnych** ścieżek (`fetch('/api/status')`) — działa tylko gdy UI i API są na tym samym hoście:porcie.
|
||||
|
||||
### Co można rozdzielić bez ryzyka
|
||||
|
||||
| Element | Zostaje na hoście | Może do Server UI |
|
||||
|---------|-------------------|-------------------|
|
||||
| `fan_controller.py` pętla NVML | **Tak** | Nie |
|
||||
| FastAPI endpoints `/api/*` | **Tak** (localhost) | Proxy z server-ui |
|
||||
| `static/index.html` | Nie | **Tak** |
|
||||
| Graceful shutdown → auto fan | **Tak** (SIGTERM w systemd) | Nie |
|
||||
|
||||
### Docelowy podział (agent)
|
||||
|
||||
```
|
||||
gpu-fan-daemon.service (root)
|
||||
└── python fan_daemon.py # lub app.py --api-only
|
||||
├── FanController.run_loop()
|
||||
└── uvicorn 127.0.0.1:18090
|
||||
└── /api/* only (bez static/)
|
||||
|
||||
server-ui (Docker lub systemd)
|
||||
└── static + proxy /api/gpu-fan/* → http://host.docker.internal:18090/api/*
|
||||
```
|
||||
|
||||
**Krytyczne:** Pętla NVML musi działać **niezależnie** od UI — pad panelu nie może zatrzymać chłodzenia.
|
||||
|
||||
---
|
||||
|
||||
## 5. Porównanie branżowe
|
||||
|
||||
### Proxmox VE (Debian appliance)
|
||||
|
||||
| Aspekt | Implementacja | Lekcja |
|
||||
|--------|---------------|--------|
|
||||
| Web UI | `pveproxy` na hoście | UI oddzielone od workloadów, ale **nie** w „zwykłym” kontenerze aplikacyjnym |
|
||||
| Operacje VM/storage | `pvedaemon`, QEMU, LVM na hoście | Privileged zawsze na host kernel |
|
||||
| Model | Single-node hypervisor | Jeden panel, wiele usług backendowych na hoście |
|
||||
|
||||
Proxmox **nie** pakuje hypervisora do Docker — control plane jest częścią OS.
|
||||
|
||||
### TrueNAS
|
||||
|
||||
| Wariant | UI | Storage / hardware |
|
||||
|---------|-----|------------------|
|
||||
| CORE | nginx + middleware na hoście | ZFS w kernelu |
|
||||
| SCALE | UI w middleware (K8s) | Dane nadal przy host storage stack |
|
||||
|
||||
Lekcja: nawet przy „nowoczesnym” SCALE ciężkie operacje zostają przy hoście.
|
||||
|
||||
### Cockpit (Ubuntu)
|
||||
|
||||
- `cockpit.socket` + moduły na hoście
|
||||
- Dostęp do `systemd`, sieci, storage przez D-Bus / polkit
|
||||
- Oficjalny wzorzec Canonical dla headless Ubuntu
|
||||
|
||||
Alternatywa: zamiast własnego server-ui — Cockpit + plugin. Minus: brak whitelist stacków AI, polityki GPU, custom NVML fan.
|
||||
|
||||
### Portainer / Dockge (homelab Docker)
|
||||
|
||||
| Element | Wzorzec |
|
||||
|---------|---------|
|
||||
| UI | Kontener |
|
||||
| Docker API | Mount `/var/run/docker.sock` |
|
||||
| Host hardware (fan, disk, systemd) | **Poza scope** — nie sterują NVML |
|
||||
|
||||
To jest **najbliższy analog** do waszej wizji dla warstwy Docker; gpu-fan wymaga **dodatkowego** host agent (jak brakujący moduł Portainera).
|
||||
|
||||
### Macierz porównawcza
|
||||
|
||||
| Produkt | UI w kontenerze? | Host privileged ops | Pasuje do gmktec-k11? |
|
||||
|---------|------------------|----------------------|------------------------|
|
||||
| Proxmox | Nie (host) | Tak | Wzorzec appliance, za ciężki |
|
||||
| TrueNAS | Częściowo | Tak | Storage-first, nie AI |
|
||||
| Cockpit | Nie | Tak | Generyczny, słaba integracja AI stacks |
|
||||
| Portainer | Tak | Tylko docker.sock | Brak gpu-fan |
|
||||
| **Server UI (propozycja)** | **Tak (docelowo)** | **Via host agent** | **Dopasowany** |
|
||||
|
||||
---
|
||||
|
||||
## 6. Macierz strategii (A–E)
|
||||
|
||||
### A — Status quo (dwa panele, host systemd)
|
||||
|
||||
- **Plus:** Działa dziś, zero refactoru
|
||||
- **Minus:** 8090 + 8091, dwa klucze, rozproszony UX
|
||||
- **Verdict:** Utrzymanie krótkoterminowe
|
||||
|
||||
### B — Split gpu-fan + UI w server-ui, oba systemd na hoście
|
||||
|
||||
- **Plus:** Jeden port 8091, mniejszy refactor niż Docker
|
||||
- **Minus:** Server-ui nadal nie w kontenerze
|
||||
- **Verdict:** **Dobry krok pośredni** (Faza 1–2 bez Fazy 3)
|
||||
|
||||
### C — Split gpu-fan daemon + server-ui w Dockerze (REKOMENDOWANE)
|
||||
|
||||
- **Plus:** Portainer pattern; jeden panel; agent tylko localhost; :8090 znika z LAN
|
||||
- **Minus:** Dockerfile, mounty, `host.docker.internal` na Linux
|
||||
- **Verdict:** **Strategia docelowa**
|
||||
|
||||
### D — Cockpit zamiast server-ui
|
||||
|
||||
- **Plus:** Dojrzały ekosystem Ubuntu
|
||||
- **Minus:** Custom plugin NVML, brak polityki GPU / whitelist compose
|
||||
- **Verdict:** Tylko przy rezygnacji z własnego UI
|
||||
|
||||
### E — gpu-fan NVML w privileged kontenerze
|
||||
|
||||
- **Plus:** Literalnie „wszystko w Dockerze”
|
||||
- **Minus:** Kruche, `Insufficient Permissions` na GeForce, ryzyko manual fan po crash
|
||||
- **Verdict:** **Odrzuć** — patrz [`gpu-fan/docs/05-DLACZEGO-NIE-DOCKER.md`](../stacks/gpu-fan/docs/05-DLACZEGO-NIE-DOCKER.md)
|
||||
|
||||
**Wybór:** **C**, z opcjonalnym **B** jako pierwsze PR (integracja UI przed dockerizacją).
|
||||
|
||||
---
|
||||
|
||||
## 7. Architektura docelowa (to-be)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
browser["Przeglądarka"]
|
||||
npm["NPMPlus :443 opcjonalnie"]
|
||||
subgraph dockerPlane [Docker control plane]
|
||||
serverUI["server-ui container :8091"]
|
||||
end
|
||||
subgraph hostAgents [Host systemd root]
|
||||
gpuAgent["gpu-fan-daemon 127.0.0.1:18090"]
|
||||
futureAgent["host-agent przyszłość"]
|
||||
end
|
||||
subgraph workloads [Docker workloads]
|
||||
comfyui["comfyui"]
|
||||
localai["localai"]
|
||||
npmplus["npmplus host network"]
|
||||
end
|
||||
gpu["RTX 3090 Ti"]
|
||||
|
||||
browser --> npm
|
||||
npm --> serverUI
|
||||
browser --> serverUI
|
||||
serverUI -->|"docker.sock"| workloads
|
||||
serverUI -->|"proxy /api/gpu-fan"| gpuAgent
|
||||
serverUI -.->|"future /api/host"| futureAgent
|
||||
gpuAgent --> gpu
|
||||
```
|
||||
|
||||
### Warstwy odpowiedzialności
|
||||
|
||||
| Warstwa | Odpowiedzialność | Technologia |
|
||||
|---------|------------------|-------------|
|
||||
| **Presentation** | Jeden dashboard, auth, nawigacja | server-ui |
|
||||
| **Orchestration** | start/stop compose, logi, polityka GPU | server-ui + `docker.sock` |
|
||||
| **Host agents** | NVML fan; przyszłe: systemd, `/data` health | systemd, API localhost |
|
||||
| **Workloads** | AI inference / image gen | istniejące stacki compose |
|
||||
|
||||
### Co NIE wchodzi do Server UI (scope)
|
||||
|
||||
- Pełne osadzenie ComfyUI / LocalAI SPA w iframe — opcjonalne; dziś wystarczą linki `:8188`, `:8070`
|
||||
- Zastąpienie NPMPlus — osobny stack reverse proxy
|
||||
- Pełny hypervisor (VM, ZFS) — poza zakresem ubuntu-bare-metal
|
||||
|
||||
---
|
||||
|
||||
## 8. Plan implementacji (fazy)
|
||||
|
||||
### Faza 1 — Rozdziel gpu-fan (host)
|
||||
|
||||
**Cel:** Agent API bez publicznego UI.
|
||||
|
||||
| Zadanie | Pliki |
|
||||
|---------|-------|
|
||||
| Wydziel tryb API-only lub `fan_daemon.py` | `stacks/gpu-fan/app.py` lub nowy moduł |
|
||||
| Env: `GPU_FAN_API_HOST=127.0.0.1`, `GPU_FAN_API_PORT=18090` | `.env.example`, `/opt/control-plane/.env` |
|
||||
| Usuń serwowanie `static/` z produkcji | `app.py` |
|
||||
| Zaktualizuj `gpu-fan.service` | `gpu-fan.service` |
|
||||
| Deprecate publiczny :8090 w `enable-lan.sh` | `scripts/enable-lan.sh` |
|
||||
| Dokumentacja | `docs/`, `README.md` |
|
||||
|
||||
**Weryfikacja:**
|
||||
```bash
|
||||
curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: $KEY"
|
||||
ss -tlnp | grep 18090 # 127.0.0.1 only
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.100.90:8090/ # connection refused
|
||||
```
|
||||
|
||||
### Faza 2 — Integracja UI w server-ui
|
||||
|
||||
| Zadanie | Pliki |
|
||||
|---------|-------|
|
||||
| Przenieś UI gpu-fan do zakładki / podstrony | `server-ui/static/gpu-fan.html` lub sekcja w `index.html` |
|
||||
| Zmień `fetch` na `/api/gpu-fan/...` | JS frontend |
|
||||
| Proxy backend | `server-ui/app.py` — `httpx` lub `requests` do agenta |
|
||||
| Jednolity `API_KEY` dla użytkownika | `.env` server-ui; agent akceptuje ten sam secret z localhost |
|
||||
| Usuń link `:8090` | `server-ui/static/index.html` |
|
||||
|
||||
**Weryfikacja:** Panel gpu-fan w `:8091`, brak potrzeby otwierania :8090.
|
||||
|
||||
### Faza 3 — Dockerize server-ui
|
||||
|
||||
| Zadanie | Pliki |
|
||||
|---------|-------|
|
||||
| `Dockerfile` | `stacks/server-ui/Dockerfile` |
|
||||
| `compose.yaml` | mount `docker.sock`, repo, env |
|
||||
| `extra_hosts: host.docker.internal:host-gateway` | compose |
|
||||
| Opcja: systemd wrapper `docker compose up -d` | `server-ui.service` lub nowy unit |
|
||||
| Whitelist w `stacks.yaml` — bez zmian | — |
|
||||
|
||||
**Weryfikacja:**
|
||||
```bash
|
||||
docker compose --profile server-ui up -d
|
||||
curl -s http://127.0.0.1:8091/api/health
|
||||
curl -s http://127.0.0.1:8091/api/stacks
|
||||
```
|
||||
|
||||
### Faza 4 — Jeden punkt wejścia (opcjonalnie)
|
||||
|
||||
- NPMPlus proxy host `panel.<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.
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
@@ -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 1024–65535
|
||||
- `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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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
|
||||
@@ -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:** 10–20 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`
|
||||
@@ -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:** 15–30 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:** 10–15 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 (~100–200 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
|
||||
@@ -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 01–03 już ukończone).
|
||||
|
||||
---
|
||||
|
||||
## Spis treści
|
||||
|
||||
1. [Filozofia: host vs Docker](#1-filozofia-host-vs-docker)
|
||||
2. [Co już masz po krokach 01–03](#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 01–03
|
||||
|
||||
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 01–03:
|
||||
|
||||
| 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
|
||||
@@ -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: 20–40 minut
|
||||
- Pierwsze uruchomienie vLLM: 20–60 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` (~8–12 GB) → `/data/docker`
|
||||
2. Pobranie modelu AWQ (~15 GB) → `/data/apps/vllm/huggingface`
|
||||
3. Ładowanie wag do VRAM — **10–30+ 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
|
||||
@@ -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 B–C 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)
|
||||
@@ -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: 10–30 minut
|
||||
- Start bez modelu: 1–2 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ć 1–2 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
|
||||
@@ -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:** 15–20 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) |
|
||||
| 1–29% | **Niedozwolone** przez API |
|
||||
| 30–100% | 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. ~50–65%).
|
||||
|
||||
### 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)
|
||||
|
||||
- 3–7 punktów
|
||||
- temperatury rosnąco, unikalne
|
||||
- prędkość: 0 lub 30–100
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -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: 10–30 minut (pierwszy raz)
|
||||
- Pierwszy start (kopia ComfyUI do `/data`): 3–10 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 | ~4–6 GB |
|
||||
| SDXL | ~8–12 GB |
|
||||
| Flux | ~12–20 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/`
|
||||
@@ -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:** 10–15 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` |
|
||||
@@ -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 |
|
||||
@@ -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 1–2
|
||||
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 |
|
||||
Executable
+421
@@ -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"
|
||||
Executable
+189
@@ -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"
|
||||
Executable
+106
@@ -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"
|
||||
@@ -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=
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -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 | ~4–6 GB |
|
||||
| SDXL | ~8–12 GB |
|
||||
| Flux | ~12–20 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)
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
docker-compose.yml
|
||||
@@ -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
|
||||
Executable
+19
@@ -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
|
||||
Executable
+16
@@ -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
|
||||
Executable
+68
@@ -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}/"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -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)
|
||||
@@ -0,0 +1,2 @@
|
||||
# DEPRECATED — use stacks/control-plane/.env.example
|
||||
# Copy: cp ../control-plane/.env.example ../control-plane/.env
|
||||
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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 **30–100%**
|
||||
- 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 |
|
||||
@@ -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, 3–7 punktów) |
|
||||
| `manual` | Stała prędkość 30–100% |
|
||||
| `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 |
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"30": 50,
|
||||
"40": 65,
|
||||
"50": 80,
|
||||
"55": 90,
|
||||
"60": 100,
|
||||
"70": 100
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"30": 50,
|
||||
"40": 65,
|
||||
"50": 80,
|
||||
"55": 90,
|
||||
"60": 100,
|
||||
"70": 100
|
||||
}
|
||||
@@ -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 Wi‑Fi/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.
|
||||
@@ -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 **30–100%** (wartości 1–29 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)
|
||||
@@ -0,0 +1,163 @@
|
||||
# Częste błędy GPU Fan
|
||||
|
||||
**Cel:** Naprawić typowe problemy bez zgadywania.
|
||||
**Czas:** zależy od problemu (2–15 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 — 3–7 punktów, temp unikalne, speed 0 lub 30–100.
|
||||
|
||||
```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)
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.32.0
|
||||
nvidia-ml-py>=12.560.0
|
||||
Executable
+51
@@ -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}"
|
||||
Executable
+80
@@ -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"
|
||||
Executable
+85
@@ -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."
|
||||
Executable
+83
@@ -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
|
||||
Executable
+74
@@ -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 8090–8099:"
|
||||
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
|
||||
@@ -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ść: 30–100% (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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
upstream/
|
||||
@@ -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)
|
||||
@@ -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` (~3–4× 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` |
|
||||
@@ -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/)
|
||||
@@ -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 | ~8–10 GB |
|
||||
| KV @ f16, ctx 8k | ~2–4 GB |
|
||||
| KV @ q8_0, ctx 8k | ~1–2 GB |
|
||||
|
||||
## TurboQuant (odłożone)
|
||||
|
||||
Backend `turboquant` + typy `turbo2`/`turbo3`/`turbo4` dają większą kompresję (~3–4×), 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/
|
||||
@@ -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).
|
||||
@@ -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) |
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
docker-compose.yml
|
||||
@@ -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
|
||||
Executable
+95
@@ -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"
|
||||
Executable
+28
@@ -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."
|
||||
Executable
+66
@@ -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}'"'"
|
||||
Executable
+16
@@ -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
Reference in New Issue
Block a user