2026-08-04 15:07:48 +02:00
---
okf: "0.1"
type: audit
visibility: private
status: active
updated: 2026-07-27
as_of: 2026-07-27
links: []
---
2026-07-27 16:59:45 +02:00
# 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=<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.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=<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`
(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):
```yaml
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
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/<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:
```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=< 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ą.