Skip to content

PostgreSQL — pg-alfa (Hetzner)

Self-hosted PG 17 + pgBouncer na Hetzner CPX42. Nahrazuje DO Managed PG (deprecated).

Pro mapování služba → DB viz db-mapping.md.

Server

  • Hostname: pg-alfa / s60-pg
  • Public IP: 46.224.173.7
  • Tailscale IP: 100.125.3.121
  • DNS: pg-alfa.studio60.cz
  • Hardware: Hetzner CPX42, 8 vCPU AMD, 16 GB RAM, 320 GB SSD, €25.49/mo

PostgreSQL 17 (port 5432)

  • Max connections: 500
  • SSL: zapnutý (self-signed; nutné pro BadWolf)
  • 🔑 pg-alfa je SENTINELŮV server — mám plný SUPERUSER. Spolehlivá cesta k SU:
    ssh root@100.125.3.121 && sudo -u postgres psql   # peer auth → superuser (usesuper=t)
    
    Tím dělám cokoli (pg_dump/restore, CREATE/DROP DATABASE, clone, role/owner změny). Před hlášením jakéhokoli DB blockeru → SSH na pg-alfa + sudo -u postgres. Viz memory feedback_pg_alfa_is_mine_su.
  • Admin user (remote): s60admin — ⚠️ credentials /root/secrets/hetzner-pg/admin.env CHYBÍ na sentinelu (zjištěno 2026-06-28). Pro remote s60admin login by se musel regenerovat (ALTER ROLE s60admin PASSWORD … přes peer auth → uložit do admin.env). Peer-auth přes SSH funguje i bez něj.
  • Clone DB (prod→dev/hub): runbooks/pg-alfa-db-clone.md

pgBouncer (port 6432)

  • Mode: transaction (pooling)
  • Max client conn: 1000
  • Default pool size: 30
  • Auth method: auth_query (od 2026-04-10) — netřeba editovat userlist.txt při přidání usera
  • Bootstrap user: pgbouncer — credentials /root/secrets/hetzner-pg/

Přístupová politika (UFW + pg_hba)

Kdo Jak Porty
Hub-alfa, prod-alfa Tailscale IP (100.125.3.121) 5432, 6432
Sentinel Tailscale IP 5432 (backup)
Cerebro (dev) Public IP (46.224.173.7) 5432, 6432 — Tailscale IPv4 routing broken (oba Hetzner IPv6)
Cortex Public IP 5432, 6432
Fess Public IP 5432, 6432
Zbytek světa DENY (UFW deny incoming)

UFW pravidla (aktuální stav, viz pg-alfa:/etc/ufw/)

default deny incoming
100.64.0.0/10        → 22, 5432, 6432   # Tailscale IPv4
fd7a:115c:a1e0::/48  → 22, 5432, 6432   # Tailscale IPv6
tailscale0           → allow all
178.63.52.57         → 5432, 6432       # Cerebro
178.104.15.77        → 5432, 6432       # Cortex (starý — po migraci odebrat)
100.64.0.0/10        → 5432, 6432       # celý Tailscale (tudy poleze cortex-new)
91.99.78.1           → 5432, 6432       # Fess (přesun VPS 2026-07-12; dřív 178.104.22.52)

pg_hba.conf

local           peer   (postgres user)
local           scram-sha-256
127.0.0.1/::1   scram-sha-256   # pgBouncer
100.64.0.0/10   scram-sha-256   # Tailscale IPv4
fd7a:115c:a1e0::/48  scram-sha-256   # Tailscale IPv6
178.63.52.57    scram-sha-256   # Cerebro
178.104.15.77   scram-sha-256   # Cortex (starý — po migraci odebrat)
100.64.0.0/10   scram-sha-256   # Tailscale — cortex-new sem spadá, NENÍ třeba nový řádek

Služby, které potřebují PG přímo (port 5432, ne pgBouncer)

Služba Důvod
N8N statement_timeout startup parameter
BadWolf SSL hardcoded (pgBouncer nepodporuje SSL passthrough)

Ostatní služby běží přes pgBouncer (6432) — viz db-mapping.md.

Debug users

Pattern: každá prod DB může mít RO debug usera (NOLOGIN ve výchozím stavu, povolit jen na dobu ladění).

Existující

  • s60_billit_debug v s60_billit_prod (heslo: /root/secrets/billit/.env.prod.debug) — ⚠️ LOGIN TRVALE aktivní pro billit dev agenta (jako badwolf; obnoveno 2026-07-12 — granty se ztratily migracemi + heslo file chyběl → reset+re-grant). BYPASSRLS (billit má 15 RLS tabulek → agent vidí napříč tenanty pro integritu). Otužení: default_transaction_read_only=on + statement_timeout=30s + SELECT-only (i budoucí tabulky přes default privileges na s60_billit_prod_user). Připojení port 5432 direct (obchází pgBouncer/6432). NEdeaktivovat — [[feedback_badwolf_debug_ro_standing]] platí analogicky.
  • s60_badwolf_debug v s60_badwolf_prod (heslo: /root/secrets/badwolf/.env.prod.debug) — pro badwolf dev agenta (cerebro), prod diagnostika/integrita; vytvořeno 2026-07-12, schválil Libor. ⚠️ LOGIN TRVALE aktivní — VÝJIMKA z NOLOGIN-when-done vzoru (Libor 2026-07-12: badwolf = živý CRM pod stálým vývojem/laděním → NEdeaktivovat). Otužení: default_transaction_read_only=on + statement_timeout=30s + SELECT-only granty (i budoucí tabulky přes default privileges na s60_badwolf_prod_user). Připojení z cerebra = stejná pg-alfa:5432 cesta jako pro s60_badwolf_dev.

Povolit / zablokovat

-- Povolit (jen na dobu ladění)
ALTER ROLE s60_billit_debug LOGIN;

-- Zablokovat zpět
ALTER ROLE s60_billit_debug NOLOGIN;

Pravidla pro debug users

  • Jen SELECT (read-only), žádný INSERT/UPDATE/DELETE
  • NOLOGIN ve výchozím stavu
  • Povolit jen na dobu ladění, pak ihned zablokovat zpět
  • Credentials NIKDY přes Relay API zprávy v plaintextu (archiv)

Fess (GraphRAG, Apache AGE) — DB fess_context

  • DB: fess_context (pg-alfa; přejmenováno z openclaw_context 2026-07-13 — openclaw deprecated). User: fess (rename z openclaw, SCRAM). Graf: fess_graph (AGE, rename z oc_graph přes nativní ag_catalog.alter_graph('...','RENAME','...')).
  • Fess connectuje z nové VPS 91.99.78.1 (pg_hba + UFW allow 5432/6432, přidáno 2026-07-13; dřív 178.104.22.52).
  • AGE write gotcha (klíčové): graph_store dělá CREATE TABLE <label> INHERITS (fess_graph._ag_label_edge/_vertex) za běhu (nové typy entit/vztahů = nové tabulky on-the-fly). Postgres u INHERITS vyžaduje, aby zakladatel VLASTNIL parent tabulky — pouhé GRANTy nestačí. Proto fess vlastní celé fess_graph (schema + všechny label tabulky + sekvence + _ag_label_vertex/_ag_label_edge) + USAGE ON ag_catalog. Ověřeno SET ROLE fess; CREATE TABLE … INHERITS. Při dalším AGE onboardingu: owner grafu = ten, kdo přes něj zapisuje, ne s60admin.
  • Fess čte i KB grafy (s60_kb, s60_cs_kb — kb_search/graph_recall), tam má jen CONNECT (read).

Migrace stav (2026-04-09)

Služba Hub Prod Dev
GlitchTip ✅ Hetzner
N8N ✅ Hetzner ✅ Hetzner
BadWolf ✅ Hetzner ✅ Hetzner ✅ Hetzner
Auth ✅ Hetzner (od 2026-04-10) ✅ Hetzner (od 2026-04-10) ✅ Hetzner
Pulse ✅ Hetzner ✅ Hetzner ✅ Hetzner
BillIt ✅ Hetzner ✅ Hetzner ✅ Hetzner
S60Mail ✅ Hetzner ✅ Hetzner ✅ Hetzner

Zálohy PG databází

  • Skript: /usr/local/bin/s60-backup.sh (cron 30 4 * * *), sync na storagebox 0 5 * * *
  • Kam: /var/backups/postgres/<db>_<YYYYMMDD_HHMM>.sql.gz, retence 14 dní
  • Jak přidat DB: dopsat do pole DATABASES=( … ) ve skriptu (⚠️ skript není ve verzování — žije jen v /usr/local/bin, zálohy stavu *.bak-<datum> vedle)
  • Credentials: /etc/s60-backup.env, dump jako s60admin na :5432 s --no-owner --no-acl

⚠️ Nová DB se do rotace NEDOSTANE sama. Kontrolní dotaz, který odhalí nezálohované databáze:

ssh root@100.125.3.121 "sudo -u postgres psql -tAc \"SELECT datname FROM pg_database WHERE NOT datistemplate AND datname NOT IN ('postgres')\"" \
  | while read db; do grep -q "$db" /usr/local/bin/s60-backup.sh || echo "NEZÁLOHOVÁNO: $db"; done

Precedent: s60_kb (KB znalostní báze, 375 MB) běžela bez jediné zálohy v rotaci, přestože je od 2026-07-31 jediným domovem dat — odhaleno až když se na to agent kb sám zeptal (T-20260731-017). Doplněno 2026-07-31.

Runbook: přidat nový DB user

Viz docs/docs/runbooks/db-user-setup.md.

Kritická pravidla

  • Nový přístup k DB (server, user, DB) → vždy schválit s Liborem (feedback_pg_access_approval.md)
  • Sentinel nesmí samostatně přidávat UFW/pg_hba pravidla
  • NIKDY nevěřit .env.example — vždy /root/secrets/