From 9eb3500b1e6141464406dffb8015e692a57eca2e Mon Sep 17 00:00:00 2001 From: oskar Date: Mon, 27 Jul 2026 16:59:45 +0200 Subject: [PATCH] =?UTF-8?q?docs(infra):=20audyt=20niezarzadzanych=20stacko?= =?UTF-8?q?w=20VPS=20=E2=80=94=20mapa=20przed=20konsolidacja=20do=20GitOps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- docs/infra/vps-audit-2026-07-27.md | 490 +++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 docs/infra/vps-audit-2026-07-27.md diff --git a/docs/infra/vps-audit-2026-07-27.md b/docs/infra/vps-audit-2026-07-27.md new file mode 100644 index 0000000..076f85d --- /dev/null +++ b/docs/infra/vps-audit-2026-07-27.md @@ -0,0 +1,490 @@ +# Audyt niezarządzanych stacków na VPS — 2026-07-27 + +Recon read-only przed konsolidacją do GitOps. Zebrane przez `ssh vps` (user `oskar`, +grupa `docker`, bez passwordless sudo) + `docker inspect` / `docker exec` (tylko odczyt) ++ kopia `database.sqlite` npm (`docker cp`, odczyt lokalny). **Żaden kontener, plik ani +wolumen nie został zmieniony.** + +Ograniczenie dostępu: katalogi `/home/dockeruser/*` mają prawa `750` (owner `dockeruser`), +więc pełnych treści `docker-compose.yml` nie dało się przeczytać jako `oskar`. +Konfiguracje poniżej są **odtworzone z `docker inspect`** (obraz, porty, wolumeny, sieci, +env, restart policy, labelki compose z `depends_on` i ścieżką pliku). Dosłowna treść +plików compose — *do weryfikacji ręcznie jako root/dockeruser*. + +--- + +## 1. TL;DR + +- **6 stacków poza GitOps**: npm (ingress!), ai-cluster (6 kontenerów), outline (3), + joplin-server (2), umami (2, **osierocony** — patrz niżej), humanai-landing+mailer + (2, uruchomione ręcznie bez compose). +- **6 portów otwartych z internetu** (potwierdzone sondą TCP z zewnątrz 2026-07-27, + brak firewalla po drodze): 22, 80, 443 (oczekiwane) oraz **81 (panel admina NPM), + 8000 (openclaw FastAPI — `/` i `/docs` odpowiadają 200 bez auth), 9100 + (node_exporter — pełne `/metrics` bez auth)**. Dodatkowo **3000 (outline)** jest + publiczne, ale ten bind jest *load-bearing*: vhost npm `outline.okit.pl` forwarduje + na `135.181.153.108:3000` (publiczny IP, pętla przez hosta), więc nie wolno go + po prostu zamknąć bez zmiany vhosta. +- **Umami jest osierocone**: katalog `/home/oskar/projects/gethumanai-infra` (compose + + `.env`) **już nie istnieje**. Kontenery działają, ale nie da się ich odtworzyć + z pliku. `APP_SECRET` i `DATABASE_URL` są odzyskiwalne wyłącznie z `docker inspect` + dopóki kontener istnieje — trzeba je zrzucić do `/opt/homelab/config/umami/.env` + ZANIM cokolwiek ruszy ten stack. +- **Żaden z audytowanych stacków nie ma `mem_limit`** (`Memory=0` wszędzie) — łamie + konwencję VPS z CLAUDE.md (4 GiB RAM, bez swapa). +- `ai-cluster-service-ops-worker` montuje **`/var/run/docker.sock` RW** — kontener + root-equivalent na hoście. +- Repo `services/npm/docker-compose.yml` to **martwy szablon**: wskazuje bind mounty + `/opt/homelab/data/npm/*`, a żywy npm używa `/home/dockeruser/docker/npm/*`. + Deploy z repo bez poprawki = npm startuje z pustą bazą → **utrata wszystkich proxy + hostów i certów w działającej instancji** (dane zostałyby na dysku, ale ingress leży). +- Sekcja CLAUDE.md „Repo-managed services on VPS" opisuje **stan docelowy, nie + faktyczny**: w repo nie ma `services/outline`, `services/joplin`, `services/ai-cluster` + ani żadnego `hosts/vps/runtime/{npm,outline,joplin,ai-cluster}/`. + +--- + +## 2. Mapa portów (stan z `ss -tlnp` + `docker inspect` + sonda zewnętrzna) + +| Port | Bind | Kontener / proces | Stack | Droga | Otwarty z internetu? | Ryzyko | +|---|---|---|---|---|---|---| +| 22 | 0.0.0.0 | sshd | host | goły | **TAK** | oczekiwane | +| 80 | 0.0.0.0 | npm | npm | ingress | **TAK** | oczekiwane | +| 443 | 0.0.0.0 | npm | npm | ingress | **TAK** | oczekiwane | +| **81** | 0.0.0.0 | npm (panel admina) | npm | goły | **TAK** | **WYSOKIE** — UI admina ingressu na świecie; do zamknięcia na tailscale/localhost | +| **8000** | 0.0.0.0 | ai-cluster-openclaw-1 (uvicorn) | ai-cluster | goły | **TAK** — `/` i `/docs` 200 bez auth | **WYSOKIE** — publiczne API + Swagger; brak vhosta w npm | +| **3000** | 0.0.0.0 | outline-outline-1 | outline | goły, ale konsumowany przez npm vhost przez publiczny IP | **TAK** | **ŚREDNIE/WYSOKIE** — powinno być tylko za npm; bind jest load-bearing (patrz §5) | +| **9100** | `*` (host netns) | node_exporter (`network_mode: host`) | fleet-prometheus (repo-managed) | goły | **TAK** — `/metrics` 200 | **ŚREDNIE** — wyciek telemetrii hosta | +| 1883 | 100.95.58.48 (tailscale) | mosquitto | ai-cluster | goły (tailscale-only) | nie | OK | +| 9090 | 100.95.58.48 | fleet-prometheus | repo-managed | tailscale-only | nie | OK | +| 18180 | 100.95.58.48 + 127.0.0.1 | control-plane-ui | repo-managed | tailscale-only | nie | OK | +| 22300 | 127.0.0.1 | joplin-server | joplin-server | za npm (vhost by-name) | nie | OK | +| 53 | 127.0.0.53 | systemd-resolved | host | — | nie | OK | +| 56906 / 34648 | tailscale IP | tailscaled | host | — | nie | OK | + +Sonda z zewnątrz potwierdziła też, że 1883/9090/18180/22300/8080 są z internetu +**zamknięte** (filtered/refused). Wniosek: nie ma firewalla tnącego 81/8000/9100 — +*do weryfikacji: czy w Hetzner Cloud jest w ogóle skonfigurowany firewall*. + +Zaszłość: aktywny vhost `gethumanai.okit.pl` forwarduje na `135.181.153.108:8080`, +gdzie **nic nie słucha** (martwy backend, zostawia 502). + +--- + +## 3. Stacki — szczegóły + +### 3.1 npm (ŻYWY INGRESS — nie ruszać) + +- Compose: `/home/dockeruser/docker/npm/docker-compose.yml` (project `npm`, + compose v5.1.2). Treść pliku nieczytelna jako oskar — poniżej rekonstrukcja. +- Obraz: `jc21/nginx-proxy-manager:latest` (build **2.14.0**, 2026-02-17). +- Restart: `unless-stopped`. `mem_limit`: **brak**. +- Porty: `80:80`, `81:81`, `443:443` — wszystkie **0.0.0.0**. +- Sieć: `npm_default` (172.19.0.0/16) — współdzielona, patrz §4. +- Env (jedyny nie-domyślny): `TZ=Europe/Warsaw`. + +Wolumeny (bind, **krytyczne — całe państwo npm**): + +| Host | Kontener | Rozmiar | +|---|---|---| +| `/home/dockeruser/docker/npm/data` | `/data` | 276 MB (w tym `database.sqlite` 155 KB, wygenerowane confy nginx) | +| `/home/dockeruser/docker/npm/letsencrypt` | `/etc/letsencrypt` | 560 KB (12 żywych certów LE: npm-13…npm-37) | + +Rekonstrukcja compose: + +```yaml +services: + npm: + image: jc21/nginx-proxy-manager:latest + container_name: npm + restart: unless-stopped + environment: + TZ: Europe/Warsaw + ports: + - "80:80" + - "81:81" + - "443:443" + volumes: + - /home/dockeruser/docker/npm/data:/data + - /home/dockeruser/docker/npm/letsencrypt:/etc/letsencrypt +``` + +Proxy hosty (z `database.sqlite`, kopia read-only; DB ostatnio zmieniona 2026-07-01; +brak WAL, więc kopia aktualna). Tylko aktywne (`is_deleted=0`): + +| ID | Domena | Forward | Cert | Uwagi | +|---|---|---|---|---| +| 1 | joplin.okit.pl | http://joplin-server:22300 | LE npm-13 | by-name przez `npm_default` | +| 2 | ha.okit.pl | http://100.108.208.3:8123 | brak (HTTP) | → PIHA (tailscale) | +| 3 | forgejo.okit.pl | http://100.108.208.3:3000 | LE npm-15 | → PIHA; **nie** VPS | +| 4 | outline.okit.pl | http://135.181.153.108:3000 | LE npm-14 | **przez publiczny IP** — pułapka §5 | +| 5 | agents.okit.pl | http://100.108.208.3:18180 | LE npm-18 | → operator-ui na PIHA | +| 7 | chz2m.kapalla.org | http://192.168.1.201:8080 | brak | LAN chelsty (spacja wiodąca w forward_host — prawdopodobnie martwy, *do weryfikacji*) | +| 8 | chha.kapala.org | http://192.168.1.241:8123 | brak | LAN chelsty | +| 10 | gethumanai.okit.pl | http://135.181.153.108:8080 | LE npm-26 | **MARTWY backend** (nic na 8080) | +| 12 | gethumanai.pl | http://humanai-landing:80; custom location `/api` → http://humanai-mailer:3000 | LE npm-36 | by-name przez `npm_default` | +| 13 | stats.gethumanai.pl | http://umami:3000 | LE npm-37 | by-name przez `npm_default` | + +Redirect: `kapala.org`, `www.kapala.org` → 302 `https://gethumanai.pl/?utm_source=…` +(cert LE npm-33). Streamów i dead hostów brak. Jeden user: `oskar@kapalla.org`. + +Uwaga: vhost `kb.kapala.org` (ostatnie commity) **nie jest** w tym npm — siedzi na +innej instancji (PIHA); tu tylko odnotowane, żeby nikt nie szukał go na VPS. + +### 3.2 ai-cluster (6 kontenerów) + +- Compose: `/home/dockeruser/docker/ai-cluster/docker-compose.yml` (project + `ai-cluster`, compose v5.1.3). Plik `.env` istnieje w katalogu stacku; nazwy + zmiennych (odczytane przez `cut -d= -f1` w kontenerze, wartości nietknięte): + `TELEGRAM_BOT_TOKEN`, `MQTT_PASSWORD`, `ALLOWED_CHAT_IDS`. +- Sieć: `ai-cluster_ai-cluster` (172.22.0.0/16); openclaw dodatkowo w `npm_default`. +- Wszystkie: restart `unless-stopped`, **bez mem_limit**. +- Obrazy `ai-cluster-*` budowane lokalnie (build cache z ~kwietnia). + +| Kontener | Obraz | Cmd | Porty | Sieci | depends_on | +|---|---|---|---|---|---| +| openclaw | ai-cluster-openclaw (local) | `uvicorn main:app --host 0.0.0.0 --port 8000` | **0.0.0.0:8000→8000** | ai-cluster, npm_default | redis, mosquitto | +| codex-worker | ai-cluster-codex-worker | `python worker.py` | — | ai-cluster | redis, mosquitto | +| planner-worker | ai-cluster-planner-worker | `python planner_worker.py` | — | ai-cluster | mosquitto | +| service-ops-worker | ai-cluster-service-ops-worker | `python service_ops_worker.py` | — | ai-cluster | mosquitto | +| redis | redis:7-alpine | `redis-server` (bez AOF) | — (expose 6379) | ai-cluster | — | +| mosquitto | eclipse-mosquitto:2 | mosquitto -c …/mosquitto.conf | **100.95.58.48:1883→1883** (tailscale-only) | ai-cluster | — | + +Env (nazwy; sekrety zamaskowane): +- openclaw: `MQTT_HOST=mosquitto`, `MQTT_PORT=1883`, `MQTT_USERNAME=codex`, + `MQTT_PASSWORD=`, `REDIS_URL=` +- codex-worker: jw. + `AGENT_ID=vps-dev-1`, `ROLE=dev`, + `GATEWAY_BASE_URL=http://piha:8080` (zależność cross-node do PIHA!), + `REQUEST_TIMEOUT_SECONDS=30` +- planner-worker: MQTT jw. + `AGENT_ID=vps-planner-1`, `ROLE=planner` +- service-ops-worker: MQTT jw. + `AGENT_ID=vps-service-ops-1`, `ROLE=service-ops`, + `COMPOSE_PROJECT_NAME=ai-cluster` + +Wolumeny / mounty: +- service-ops-worker: `/home/dockeruser/docker/ai-cluster/.env → /app/.env` (ro), + `…/docker-compose.yml → /app/docker-compose.yml` (ro), + **`/var/run/docker.sock → /var/run/docker.sock` (RW)** — root-equivalent. +- mosquitto: bind ro `/home/dockeruser/docker/ai-cluster/mosquitto → /mosquitto/config` + (zawiera `mosquitto.conf`, `passwd` 0600, `acl` 0600 — **krytyczny zestaw do + zachowania**); dane i logi na **anonimowych** wolumenach (`ab200e89…` i `9551f9e4…`, + oba 0 B — brak retained/persystencji do stracenia). +- redis: anonimowy wolumen `4f60cb72…` (3.5 kB). + +Konfig mosquitto: `listener 1883`, `allow_anonymous false`, password_file + acl_file. + +Ryzyka: publiczny 8000 bez auth (+ otwarty Swagger `/docs`); docker.sock; +anonimowe wolumeny znikną przy `compose down -v` / zmianie projektu. +Per CLAUDE.md compute-workery docelowo na SOLARIA. + +### 3.3 outline (3 kontenery) + +- Compose: `/home/dockeruser/docker/outline/docker-compose.yml` (project `outline`, + compose v5.1.3). Czy env z `.env` czy inline — *do weryfikacji* (plik nieczytelny). +- Sieć: **tylko** `outline_outline_internal` (172.21.0.0/16) — outline NIE jest w + `npm_default`, dlatego vhost idzie przez publiczny IP. + +| Kontener | Obraz | Porty | Wolumen | Rozmiar | +|---|---|---|---|---| +| outline | outlinewiki/outline:**1.6.1** | **0.0.0.0:3000→3000** | `outline_outline_storage → /var/lib/outline/data` | 3.3 MB | +| postgres | postgres:16-alpine | — | `outline_postgres_data` | 69 MB | +| redis | redis:7-alpine (`--appendonly yes`) | — | `outline_redis_data` | 25 MB | + +depends_on: outline → postgres (healthy), redis (healthy). + +Env outline (nazwy; wartości sekretów zamaskowane): `URL=https://outline.okit.pl`, +`FORCE_HTTPS=true`, `PORT=3000`, `FILE_STORAGE=local`, +`FILE_STORAGE_LOCAL_ROOT_DIR=/var/lib/outline/data`, `PGSSLMODE=disable`, +`ALLOWED_DOMAINS=gmail.com`, `NODE_ENV=production`, +`GOOGLE_CLIENT_ID=84321112439-….apps.googleusercontent.com`, +`GOOGLE_CLIENT_SECRET=`, +`SECRET_KEY=`, `UTILS_SECRET=`, +`DATABASE_URL=`, `REDIS_URL=`, +`SMTP_HOST/PORT/USERNAME/PASSWORD/FROM_EMAIL/REPLY_EMAIL/SECURE=`, +`SLACK_KEY/SLACK_SECRET=`, komplet pustych/ustawionych `OIDC_*` +(`OIDC_CLIENT_SECRET`, `OIDC_AUTH_URI`, `OIDC_TOKEN_URI` maskowane). + +Vhost: `outline.okit.pl` → `http://135.181.153.108:3000` (§3.1, poz. 4). + +### 3.4 joplin-server (2 kontenery) + +- Compose: `/home/dockeruser/docker/joplin-server/docker-compose.yml` + (project `joplin-server`, compose v5.1.2/5.1.3). +- Sieci: `joplin-net` (172.20.0.0/16) + app dodatkowo w `npm_default`. + +| Kontener | Obraz | Porty | Wolumen | Rozmiar | +|---|---|---|---|---| +| joplin-server (service `app`) | joplin/server:**latest** | **127.0.0.1:22300→22300** | — | — | +| joplin-db (service `db`) | postgres:**18** | — | `joplin_postgres_data → /var/lib/postgresql` | 423 MB | + +depends_on: app → db (healthy). + +Env app: `APP_BASE_URL=https://joplin.okit.pl`, `APP_PORT=22300`, `TRUST_PROXY=1`, +`DB_CLIENT=pg`, `POSTGRES_HOST=db`, `POSTGRES_PORT=5432`, +`POSTGRES_USER/DATABASE/DB=joplin`, `POSTGRES_PASSWORD=`. + +Quirk: kontener app ma nadpisane `command` — inline skrypt node łatający +`http.Server.listen`, żeby serwer słuchał na `0.0.0.0` w kontenerze (obejście +znanego problemu joplin-server). Przy przenoszeniu compose musi zachować to +`command:` — inaczej app wstanie na localhost w kontenerze i npm go nie dosięgnie. + +Vhost: `joplin.okit.pl` → `http://joplin-server:22300` (by-name, wymaga `npm_default`). + +Uwaga na `postgres:18` + mount na `/var/lib/postgresql` (nie `…/data`) — nowy layout +wolumenu Postgresa 18; nie zmieniać ścieżki przy migracji. + +### 3.5 umami (2 kontenery) — OSIEROCONY + +- Compose był w `/home/oskar/projects/gethumanai-infra/services/umami/` z `.env` + (labelki `environment_file` to potwierdzają; compose v2.27.0 — stary binarny + docker-compose). **Katalog `/home/oskar/projects/` już nie istnieje** — stack nie + jest odtwarzalny z dysku. Kontenery żyją tylko dzięki `restart: unless-stopped`. +- Sieci: umami w `npm_default` + `umami_internal` (172.26.0.0/16); db tylko internal. + +| Kontener | Obraz | Porty | Wolumen | Rozmiar | +|---|---|---|---|---| +| umami | ghcr.io/umami-software/umami:postgresql-latest | — (expose 3000) | — | — | +| umami-db | postgres:16-alpine | — | `umami_umami-db-data` | 67 MB | + +Env umami: `DATABASE_TYPE=postgresql`, `DATABASE_URL=`, +`APP_SECRET=`, `PORT=3000`, `HOSTNAME=0.0.0.0`. +Env umami-db: `POSTGRES_DB/USER=umami`, `POSTGRES_PASSWORD=`. + +Rekonstrukcja compose (do wciągnięcia do repo): + +```yaml +services: + umami: + image: ghcr.io/umami-software/umami:postgresql-latest + container_name: umami + restart: unless-stopped + environment: + DATABASE_TYPE: postgresql + DATABASE_URL: # postgres://umami:…@umami-db:5432/umami + APP_SECRET: + networks: [npm_default, internal] + depends_on: + umami-db: {condition: service_healthy} + umami-db: + image: postgres:16-alpine + container_name: umami-db + restart: unless-stopped + environment: + POSTGRES_DB: umami + POSTGRES_USER: umami + POSTGRES_PASSWORD: + volumes: + - umami-db-data:/var/lib/postgresql/data + networks: [internal] +volumes: + umami-db-data: # UWAGA: żywy wolumen nazywa się umami_umami-db-data +networks: + npm_default: {external: true} + internal: {} +``` + +Vhost: `stats.gethumanai.pl` → `http://umami:3000` (by-name). + +**Pilne przed czymkolwiek innym przy tym stacku**: zrzucić sekrety z +`docker inspect umami umami-db` do `/opt/homelab/config/umami/.env`. Jeśli kontener +zostanie usunięty przed tym zrzutem, `APP_SECRET` (podpisy sesji) przepada. + +### 3.6 humanai-landing + humanai-mailer — patrz §6 (kontenery ręczne) + +--- + +## 4. Współdzielone zasoby + +1. **Sieć `npm_default` = wspólna szyna ingressu.** Członkowie: npm, joplin-server, + umami, humanai-landing, humanai-mailer, **openclaw**. Trzy vhosty (joplin, + gethumanai.pl + /api, stats) resolwują backendy **po nazwie kontenera** w tej + sieci. Konsekwencje: + - sieci nie da się usunąć, dopóki wisi na niej 5 cudzych kontenerów (dobra + zapora przed przypadkowym `compose down` niszczącym ingress); + - każdy przenoszony stack musi po migracji nadal być w sieci osiągalnej przez + npm **pod tą samą nazwą kontenera**; + - w GitOps-owym compose npm sieć powinna być zadeklarowana tak, żeby zachować + **nazwę** `npm_default` (project `npm` + default network, albo + `networks: {default: {name: npm_default}}`). + - openclaw siedzi w `npm_default` mimo braku vhosta — zaszłość, *do weryfikacji* + czy potrzebne. +2. **`ai-cluster_ai-cluster`**: workers + redis + mosquitto + openclaw. +3. **mosquitto (VPS) na tailscale 1883** — broker dostępny dla całego mesha; workers + ai-cluster używają go po nazwie, ale klienci spoza hosta mogą wchodzić po + `100.95.58.48:1883` (auth wymagane). *Do weryfikacji: kto poza ai-cluster używa.* + To jest INNY broker niż `services/mosquitto` (chelsty) w repo. +4. **Bazy danych NIE są współdzielone między stackami** — każdy stack ma własny + postgres (outline pg16, joplin pg18, umami pg16) i własny redis (outline, ai-cluster). + To upraszcza rozdzielne przenoszenie. +5. **Zależność cross-node**: codex-worker → `http://piha:8080` (llm-gateway na PIHA). +6. **docker.sock** w service-ops-worker — współdzielony dostęp do demona Dockera. +7. Wolumeny anonimowe: 3 w użyciu (mosquitto data/log — puste; ai-cluster redis — + 3.5 kB) + **5 osieroconych** (0 links: `2bebc404…`, `a1849a75…`, `ab6fd040…`, + `b7c791d3…`, `c76234b3…`) — kandydaci do sprzątnięcia, *do weryfikacji osobno*. + +--- + +## 5. Rekomendowana kolejność wciągania do repo + +Właściciel chce **npm pierwszy**. Ocena: **wykonalne bezpiecznie**, pod warunkami +z 5.1. npm nie ma zależności od innych stacków (to inni zależą od niego), stan jest +w dwóch bind mountach, a cutover to sekundy przerwy. Największe ryzyko to błąd +ścieżek wolumenów — obecny plik w repo dokładnie ten błąd zawiera. + +### 5.1 npm (pierwszy — fundament, zamyka port 81) + +Co zachować: bind mounty `/home/dockeruser/docker/npm/{data,letsencrypt}` **w miejscu** +(zgodnie z regułą CLAUDE.md „data paths stay in place at cutover"). Certy odnawia +wbudowany certbot — katalog letsencrypt musi przeżyć 1:1. + +Pułapki: +1. **Repo `services/npm/docker-compose.yml` wskazuje `/opt/homelab/data/npm/*`** — + trzeba to zmienić (w samym compose albo w `hosts/vps/runtime/npm/…override.yml`, + który dziś NIE istnieje) na `/home/dockeruser/docker/npm/*`. Bez tego npm wstaje + pusty: zero vhostów, zero certów → cały ingress leży mimo że dane są na dysku. +2. **Nazwa sieci**: żywa sieć to `npm_default` z 5 cudzymi kontenerami. GitOps-owy + deploy musi użyć **tej samej** sieci (project name `npm` albo jawne + `name: npm_default`). Compose nie usunie sieci z podpiętymi kontenerami, ale + przy złej konfiguracji utworzy nową i npm straci by-name backendy (joplin, + umami, gethumanai.pl, /api). +3. **Cutover wymaga zatrzymania starego kontenera** (ten sam `container_name` i + porty). Sekwencja: `docker stop npm` (stary) → `docker compose up -d` z repo → + healthcheck + test 2–3 vhostów. Rollback: `docker start npm` w starym katalogu. + To jest jedyna dopuszczalna przerwa (sekundy). Wymaga zgody operatora — poza + zakresem tego audytu. +4. **Zamknięcie 81**: w override zbindować `100.95.58.48:81:81` (+ ew. + `127.0.0.1:81:81`). NIE zostawiać `81:81`. +5. Dodać `mem_limit` (npm dziś bez limitu; propozycja 256–512 MB) i rozważyć + `oom_score_adj: -900` — npm to de facto najbardziej krytyczny kontener na hoście. +6. Stary katalog compose zostaje jako rollback do czasu stabilizacji; potem + oznaczyć jako wycofany (nie usuwać danych!). + +### 5.2 umami (drugi — bo konfiguracja już nie istnieje) + +Nie dlatego, że ważny, tylko dlatego, że **jest jeden incydent od utraty sekretów** +(restart hosta przeżyje, ale `docker rm` już nie). Kroki: zrzut env z inspect → +`/opt/homelab/config/umami/.env` → compose w repo wg rekonstrukcji §3.5 → +cutover. Pułapki: nazwa wolumenu musi zmapować się na istniejący +`umami_umami-db-data` (project name `umami`!); kontener `umami` musi zostać +w `npm_default` pod tą samą nazwą (vhost stats.gethumanai.pl). + +### 5.3 joplin-server (trzeci — najprostszy) + +Zachować: wolumen `joplin_postgres_data` (project name `joplin-server`!, mount na +`/var/lib/postgresql` — layout pg18), sieć `npm_default` dla app, bind +`127.0.0.1:22300`, **nadpisane `command`** (patcha listen z §3.4). Ryzyko niskie. +Dodatkowo: przypiąć tag obrazu (dziś `joplin/server:latest` i `postgres:18` — +`latest` na produkcyjnej bazie to ryzyko samo w sobie). + +### 5.4 outline (czwarty — zamyka publiczny 3000) + +Tu jest sprzężenie z npm — trzy zmiany muszą pójść **razem**: +1. dodać kontener outline do sieci osiągalnej z npm (np. `npm_default`), +2. przepiąć vhost `outline.okit.pl` z `135.181.153.108:3000` na `http://outline:3000` + (jedyna w tym planie zmiana w konfiguracji npm — przez UI/API, nie przez sqlite), +3. zdjąć publiczny bind (usunąć `ports:` albo `127.0.0.1:3000:3000`). + +Kolejność: (1) → (2) → test → (3). Wtedy 3000 znika ze świata bez przerwy w działaniu. +Zachować: wszystkie 3 named volumes `outline_*` (project name `outline`!). Zmiana +project name = compose tworzy NOWE puste wolumeny — najprostszy sposób na „zniknięcie" +wiki. Wersja przypięta 1.6.1 — zostawić, upgrade osobnym tematem. + +### 5.5 humanai-landing + humanai-mailer (piąty) + +Złożyć w jeden compose w repo (dziś: landing ma compose na dysku, ale kontener +uruchomiony ręcznie i rozjechany z plikiem; mailer nie ma nic). Wymaga odzyskania +źródła mailera (*do weryfikacji: repo na Forgejo*) — obrazy są budowane lokalnie. +Przy okazji: usunąć/naprawić martwy vhost `gethumanai.okit.pl` (→ nieistniejący +:8080). Dane: brak wolumenów — nic do zachowania poza env SMTP (zrzut z inspect, +jak przy umami). + +### 5.6 ai-cluster (ostatni — największy i architektonicznie do przebudowy) + +Powody na koniec: 6 kontenerów, docker.sock, lokalne buildy, a per CLAUDE.md +compute-workery mają docelowo iść na SOLARIA — nie warto cementować obecnego +kształtu w repo dwa razy. Co zachować: bind `/home/dockeruser/docker/ai-cluster/` +(`.env` + `mosquitto/{mosquitto.conf,passwd,acl}` — bez passwd/acl broker odetnie +wszystkich klientów). Przy migracji: anonimowe wolumeny → nazwane (dane do stracenia +~0 B, więc bez migracji danych). **Niezależnie od kolejności: publiczny 8000 do +zamknięcia szybciej** (bind na `100.95.58.48:8000` albo vhost z auth w npm — decyzja +właściciela; dziś każdy z internetu widzi Swaggera openclaw). + +### Ryzyka wspólne dla wszystkich cutoverów + +- Każdy stack dostaje `mem_limit` w `hosts/vps/runtime//…override.yml` + (dziś suma limitów = 0; budżet 3.1 GiB per CLAUDE.md; RAM: 3.8 GiB, ~1.7 GiB used). +- Project name przy `docker compose up` MUSI odpowiadać staremu (wolumeny + `_`, sieci `_default`). +- Nigdy `docker compose down -v` na starych stackach. +- Zaszłość compose v2.27 (umami) vs v5.x (reszta) — bez znaczenia po przejściu na + repo, byle project name się zgadzał. + +--- + +## 6. Kontenery ręczne (bez compose) — rekonstrukcja z `docker inspect` + +### 6.1 humanai-landing + +- Obraz: `humanai-landing:latest` — build lokalny 2026-06-25, multi-stage + (Astro build → nginx:alpine). Źródło: `/home/oskar/gethumanai-landing` + (git, origin: Forgejo na PIHA `ssh://git@100.108.208.3:222/oskar/gethumanai-landing.git`; + deploy skryptem `deploy.sh` z SATURN). +- Kontener NIE odpowiada plikowi `docker-compose.yml` leżącemu w tym katalogu + (plik przewiduje `ports: 8080:80`; żywy kontener nie publikuje nic i nie ma + labelek compose → uruchomiony ręcznie). Stąd martwy vhost `gethumanai.okit.pl → :8080`. +- Rekonstrukcja: + +```bash +docker run -d --name humanai-landing \ + --restart unless-stopped \ + --network npm_default \ + humanai-landing:latest +# nginx słucha na :80 w kontenerze; ruch wyłącznie przez npm (vhost gethumanai.pl) +``` + +- Brak wolumenów i env — cała treść w obrazie. Nginx w obrazie: statyczny serwing + `/usr/share/nginx/html` + security headers + gzip (konfig w repo źródłowym). + +### 6.2 humanai-mailer + +- Obraz: `humanai-mailer:latest` — build lokalny 2026-06-24 (node:22). Proces: + `node mailer.mjs`, słucha na `:3000` w kontenerze (potwierdzone przez + `/proc/net/tcp6`). **Źródła NIE znaleziono na dysku VPS** — *do weryfikacji* + (prawdopodobnie repo na Forgejo). +- Konsumowany wyłącznie przez npm: vhost `gethumanai.pl`, custom location + `/api` → `http://humanai-mailer:3000`. +- Rekonstrukcja: + +```bash +docker run -d --name humanai-mailer \ + --restart unless-stopped \ + --network npm_default \ + -e SMTP_HOST= \ + -e SMTP_PORT= \ + -e SMTP_USER= \ + -e SMTP_PASS= \ + humanai-mailer:latest +``` + +- Brak wolumenów. Wartości SMTP odzyskiwalne z `docker inspect humanai-mailer` + (zrzut do `/opt/homelab/config/` przed jakimkolwiek ruszaniem kontenera — + jak przy umami). + +--- + +## 7. Czego nie dało się ustalić (do weryfikacji ręcznie) + +1. Dosłowna treść czterech compose pod `/home/dockeruser/docker/` (prawa 750; + potrzebny root/dockeruser). Rekonstrukcje z inspect są funkcjonalnie kompletne, + ale komentarze/`env_file`/healthchecki zdefiniowane w plikach — nieznane. +2. Czy `/home/dockeruser/docker/` zawiera COŚ WIĘCEJ niż npm/ai-cluster/outline/ + joplin-server (listing niedostępny). +3. Czy istnieją `.env` przy npm/outline/joplin i co zawierają (nazwy zmiennych). +4. Firewall Hetzner Cloud — sonda wskazuje brak filtrowania (81/8000/9100 otwarte); + sprawdzić w panelu Hetznera. +5. Źródło humanai-mailer (Forgejo?) i czy landing w kontenerze == HEAD repo. +6. Kto/co konsumuje `openclaw:8000` z internetu — czy można zbindować na tailscale. +7. Vhost `chz2m.kapalla.org` (spacja wiodąca w forward_host `' 192.168.1.201'`) — + czy w ogóle działa. +8. 5 osieroconych anonimowych wolumenów — czyje, czy do kasacji. +9. `gokapi` zadeklarowane w `hosts/vps/services.yaml` (exposure: public, :53842), + a nie biega na VPS — rozjazd manifestu z rzeczywistością.