491 lines
26 KiB
Markdown
491 lines
26 KiB
Markdown
|
|
# 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ą.
|