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>
26 KiB
| 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.sqlitenpm (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/docsodpowiadają 200 bez auth), 9100 (node_exporter — pełne/metricsbez auth). Dodatkowo 3000 (outline) jest publiczne, ale ten bind jest load-bearing: vhost npmoutline.okit.plforwarduje na135.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_SECRETiDATABASE_URLsą odzyskiwalne wyłącznie zdocker inspectdopóki kontener istnieje — trzeba je zrzucić do/opt/homelab/config/umami/.envZANIM cokolwiek ruszy ten stack.
- Żaden z audytowanych stacków nie ma
mem_limit(Memory=0wszędzie) — łamie konwencję VPS z CLAUDE.md (4 GiB RAM, bez swapa). ai-cluster-service-ops-workermontuje/var/run/docker.sockRW — kontener root-equivalent na hoście.- Repo
services/npm/docker-compose.ymlto 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-clusterani żadnegohosts/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(projectnpm, 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 /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(projectai-cluster, compose v5.1.3). Plik.envistnieje w katalogu stacku; nazwy zmiennych (odczytane przezcut -d= -f1w 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 wnpm_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(zawieramosquitto.conf,passwd0600,acl0600 — krytyczny zestaw do zachowania); dane i logi na anonimowych wolumenach (ab200e89…i9551f9e4…, 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(projectoutline, compose v5.1.3). Czy env z.envczy inline — do weryfikacji (plik nieczytelny). - Sieć: tylko
outline_outline_internal(172.21.0.0/16) — outline NIE jest wnpm_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.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(projectjoplin-server, compose v5.1.2/5.1.3). - Sieci:
joplin-net(172.20.0.0/16) + app dodatkowo wnpm_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.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(labelkienvironment_fileto 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ękirestart: 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.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
- 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 downniszczą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(projectnpm+ default network, albonetworks: {default: {name: npm_default}}). - openclaw siedzi w
npm_defaultmimo braku vhosta — zaszłość, do weryfikacji czy potrzebne.
- sieci nie da się usunąć, dopóki wisi na niej 5 cudzych kontenerów (dobra
zapora przed przypadkowym
ai-cluster_ai-cluster: workers + redis + mosquitto + openclaw.- 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. - 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.
- Zależność cross-node: codex-worker →
http://piha:8080(llm-gateway na PIHA). - docker.sock w service-ops-worker — współdzielony dostęp do demona Dockera.
- 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:
- Repo
services/npm/docker-compose.ymlwskazuje/opt/homelab/data/npm/*— trzeba to zmienić (w samym compose albo whosts/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. - Nazwa sieci: żywa sieć to
npm_defaultz 5 cudzymi kontenerami. GitOps-owy deploy musi użyć tej samej sieci (project namenpmalbo jawnename: 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). - Cutover wymaga zatrzymania starego kontenera (ten sam
container_namei porty). Sekwencja:docker stop npm(stary) →docker compose up -dz repo → healthcheck + test 2–3 vhostów. Rollback:docker start npmw starym katalogu. To jest jedyna dopuszczalna przerwa (sekundy). Wymaga zgody operatora — poza zakresem tego audytu. - Zamknięcie 81: w override zbindować
100.95.58.48:81:81(+ ew.127.0.0.1:81:81). NIE zostawiać81:81. - 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. - 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:
- dodać kontener outline do sieci osiągalnej z npm (np.
npm_default), - przepiąć vhost
outline.okit.plz135.181.153.108:3000nahttp://outline:3000(jedyna w tym planie zmiana w konfiguracji npm — przez UI/API, nie przez sqlite), - zdjąć publiczny bind (usunąć
ports:albo127.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_limitwhosts/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 upMUSI odpowiadać staremu (wolumeny<project>_<volume>, sieci<project>_default). - Nigdy
docker compose down -vna 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 PIHAssh://git@100.108.208.3:222/oskar/gethumanai-landing.git; deploy skryptemdeploy.shz SATURN). - Kontener NIE odpowiada plikowi
docker-compose.ymlleżącemu w tym katalogu (plik przewidujeports: 8080:80; żywy kontener nie publikuje nic i nie ma labelek compose → uruchomiony ręcznie). Stąd martwy vhostgethumanai.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:3000w 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:
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)
- 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. - Czy
/home/dockeruser/docker/zawiera COŚ WIĘCEJ niż npm/ai-cluster/outline/ joplin-server (listing niedostępny). - Czy istnieją
.envprzy npm/outline/joplin i co zawierają (nazwy zmiennych). - Firewall Hetzner Cloud — sonda wskazuje brak filtrowania (81/8000/9100 otwarte); sprawdzić w panelu Hetznera.
- Źródło humanai-mailer (Forgejo?) i czy landing w kontenerze == HEAD repo.
- Kto/co konsumuje
openclaw:8000z internetu — czy można zbindować na tailscale. - Vhost
chz2m.kapalla.org(spacja wiodąca w forward_host' 192.168.1.201') — czy w ogóle działa. - 5 osieroconych anonimowych wolumenów — czyje, czy do kasacji.
gokapizadeklarowane whosts/vps/services.yaml(exposure: public, :53842), a nie biega na VPS — rozjazd manifestu z rzeczywistością.