homelab-codex-ws/kb/audits/vps-stacki-2026-07-27.md
oskar 9f77a723e8 feat(kb): 5 audytow/reconow -> kb/audits/ (type: audit, as_of)
czujniki-2026-07-30 (node-agent vs stability-agent)
lustro-shipping-2026-07-16 (event=dead prom=up, 1507 mismatchy)
prometheus-cutover-2026-07-06 (recon starego toru livenesci)
piha-slim-2026-07-02 (audyt odchudzania PIHA)
vps-stacki-2026-07-27 (audyt niezarzadzanych stackow na VPS)

ODSTEPSTWO OD RECONU — swiadome. Recon typowal te 5 plikow jako SPLIT
(audit+decision / audit+incident / audit+phase). Rozstrzygniecie 2 wprowadza
typ `audit` z polem as_of i mapuje kazdy z nich na JEDNA sciezke
kb/audits/<obszar>-<data>.md. Audyt jest spojna migawka stanu z konkretna
data — rozbicie go na "ustalenia" i "rekomendacje" rozerwaloby ten kontekst
i wymagaloby redakcji tresci, czego etap 2 zabrania. Zostaja w calosci.

Efekt: 29 SPLIT-ow z reconu realizowane jako 24 (10 service+runbook,
14 wielotypowych), 5 zamienionych na caloscowe dokumenty type: audit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 16:58:46 +02:00

26 KiB
Raw Blame History

okf type visibility status updated as_of links
0.1 audit private active 2026-07-27 2026-07-27

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:

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 /apihttp://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=<SECRET:MQTT_PASSWORD>, REDIS_URL=<SECRET: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:GOOGLE_CLIENT_SECRET>, SECRET_KEY=<SECRET:SECRET_KEY>, UTILS_SECRET=<SECRET:UTILS_SECRET>, DATABASE_URL=<SECRET:DATABASE_URL>, REDIS_URL=<SECRET:REDIS_URL>, SMTP_HOST/PORT/USERNAME/PASSWORD/FROM_EMAIL/REPLY_EMAIL/SECURE=<SECRET:SMTP_*>, SLACK_KEY/SLACK_SECRET=<SECRET:SLACK_*>, komplet pustych/ustawionych OIDC_* (OIDC_CLIENT_SECRET, OIDC_AUTH_URI, OIDC_TOKEN_URI maskowane).

Vhost: outline.okit.plhttp://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=<SECRET: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.plhttp://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=<SECRET:DATABASE_URL>, APP_SECRET=<SECRET:APP_SECRET>, PORT=3000, HOSTNAME=0.0.0.0. Env umami-db: POSTGRES_DB/USER=umami, POSTGRES_PASSWORD=<SECRET:POSTGRES_PASSWORD>.

Rekonstrukcja compose (do wciągnięcia do repo):

services:
  umami:
    image: ghcr.io/umami-software/umami:postgresql-latest
    container_name: umami
    restart: unless-stopped
    environment:
      DATABASE_TYPE: postgresql
      DATABASE_URL: <SECRET:DATABASE_URL>   # postgres://umami:…@umami-db:5432/umami
      APP_SECRET: <SECRET: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: <SECRET: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.plhttp://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 23 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 256512 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:18latest 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/<svc>/…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 <project>_<volume>, sieci <project>_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:
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 VPSdo weryfikacji (prawdopodobnie repo na Forgejo).
  • Konsumowany wyłącznie przez npm: vhost gethumanai.pl, custom location /apihttp://humanai-mailer:3000.
  • Rekonstrukcja:
docker run -d --name humanai-mailer \
  --restart unless-stopped \
  --network npm_default \
  -e SMTP_HOST=<SECRET:SMTP_HOST> \
  -e SMTP_PORT=<SECRET:SMTP_PORT> \
  -e SMTP_USER=<SECRET:SMTP_USER> \
  -e SMTP_PASS=<SECRET: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ą.