Adds a dependency-free liveness endpoint (GET /api/health), a Docker healthcheck against it, and docker/rolling-deploy.sh which rebuilds the shared image once and restarts pawfeed-1/2/3 sequentially, gated on each becoming healthy before moving to the next. Verified zero-downtime via continuous curl monitoring through a full run against production.
9.0 KiB
Scaling-Roadmap: 2–3 Container-Replicas + NPM Load Balancing
Status: ✅ Live seit 2026-08-16. Alle 4 Schritte umgesetzt und per Lasttest verifiziert (CPU verteilt sich jetzt ~100% auf jede der 3 Replicas statt 133% auf einem Kern). Kompatibilitäts-Alias und alter Einzelcontainer sind entfernt — der finale Zielzustand aus Schritt 2 läuft unverändert in Produktion. Dieses Dokument bleibt als Runbook/Entscheidungs-Log stehen, falls die Config mal reproduziert oder debuggt werden muss.
Kontext: Gitea #26. Lasttest am 2026-08-15 zeigt eine harte Durchsatz-Decke bei ~60–65 Req/Sek. Ursache: docker/Dockerfile:78 startet PawFeed als einzelnen node server.js-Prozess (Next.js Standalone ist single-process). CPU-Last dabei: 133% von 400% verfügbar (4-Kern-NAS, i5-6500 @ 3.6GHz) — ein Kern ist ausgelastet, drei liegen brach. Statt eines zweiten physischen Servers + externem Loadbalancer: die 3 ungenutzten Kerne auf derselben Maschine per Container-Replicas + dem bereits vorhandenen Nginx Proxy Manager (NPM) nutzen.
Aufwand-Schätzung: ~1,5–2 Stunden, additiv (kein Breaking Change), Rollback = docker-compose.yml zurücksetzen.
Schritt 1 — Bestandsaufnahme (10 Min) — ✅ erledigt 2026-08-16
- NPM-Weboberfläche: Forward Hostname/Port für
pawfeed.orgisthttp://pawfeed:3000über das sharednginx_default-Network (Container-DNS-Name, kein Host-Port-Mapping) — Annahme bestätigt. Achtung: Nach dem Umbau aufpawfeed-1/2/3gibt es keinen Container mehr namenspawfeed— Compose-Deploy und NPM-Umstellung müssen im selben Wartungsfenster passieren, sonst Downtime. - Custom Locations in NPM: noch nichts hinterlegt.
- Supabase-Dashboard → Database → Connection Pooling: Shared-Modus, Pool Size 15, Max Client Connections 200 (Upgrade nötig für mehr). 3 Replicas ×
DB_POOL_MAX=10(indocker-compose.ymlgesetzt) = 30 gleichzeitige Verbindungen, deutlich unter dem Limit — keine weitere Anpassung nötig. - Neuer Befund: Ein
upstream {}-Block ist nur im Nginx-http{}-Kontext gültig. NPMs Advanced-Tab pro Proxy Host schreibt aber nur in denserver{}-Kontext — der Upstream-Block lässt sich nicht über die Weboberfläche einrichten, sondern braucht eine Datei unter/data/nginx/custom/http_top.confauf dem Docker-Volume des NPM-Containers (Dateisystem-Zugriff auf die NAS nötig). Siehedocker/npm-upstream.conf.
Schritt 2 — docker-compose.yml auf 3 Replicas umstellen (20–30 Min)
Explizite Services (pawfeed-1/2/3) statt docker compose up --scale — feste, vorhersehbare Hostnamen sind fürs NPM-Upstream nötig. YAML-Anchor vermeidet dreifache Duplizierung der Build-Args/Env:
x-pawfeed-common: &pawfeed-common
build:
context: ..
dockerfile: docker/Dockerfile
args:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
NEXT_PUBLIC_SENTRY_DSN: ${NEXT_PUBLIC_SENTRY_DSN}
SENTRY_ORG: ${SENTRY_ORG}
SENTRY_PROJECT: ${SENTRY_PROJECT}
SENTRY_AUTH_TOKEN: ${SENTRY_AUTH_TOKEN}
SENTRY_RELEASE: ${SENTRY_RELEASE}
image: pawfeed:latest
restart: unless-stopped
env_file:
- .env
environment:
NODE_ENV: production
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
depends_on:
- redis
networks:
- default
- nginx_default
services:
redis:
image: redis:7-alpine
container_name: pawfeed-redis
restart: unless-stopped
volumes:
- redis_data:/data
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
pawfeed-1:
<<: *pawfeed-common
container_name: pawfeed-1
pawfeed-2:
<<: *pawfeed-common
container_name: pawfeed-2
pawfeed-3:
<<: *pawfeed-common
container_name: pawfeed-3
volumes:
redis_data:
networks:
default:
nginx_default:
external: true
Redis bleibt einfach (bereits geteilter State über alle Replicas — richtig so, nicht anfassen).
Schritt 3 — NPM Load Balancing konfigurieren (20–30 Min)
Zwei Teile, unterschiedliche Nginx-Kontexte — geht nicht komplett über die NPM-Weboberfläche:
-
Upstream-Block (
docker/npm-upstream.conf) muss als Datei/data/nginx/custom/http_top.confauf dem Docker-Volume des NPM-Containers liegen —upstream {}ist nur imhttp{}-Kontext gültig, den NPMs Advanced-Tab nicht erreicht. Braucht Dateisystem-/SSH-Zugriff auf die NAS. NPM zieht Dateien unter/data/nginx/custom/automatisch beim nächsten Config-Reload ein.upstream pawfeed_upstream { least_conn; server pawfeed-1:3000 max_fails=3 fail_timeout=30s; server pawfeed-2:3000 max_fails=3 fail_timeout=30s; server pawfeed-3:3000 max_fails=3 fail_timeout=30s; } -
Proxy-Pass-Override im Proxy-Host für
pawfeed.org, Advanced-Tab: eigenerlocation /-Block statt dem generierten Zielpawfeed:3000(das nach dem Umbau ohnehin nicht mehr existiert):location / { proxy_pass http://pawfeed_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
least_conn verteilt auf die Instanz mit den wenigsten aktiven Verbindungen — passender als reines Round-Robin bei ungleich langen Requests (z. B. Video-Uploads vs. einfache GETs).
Reihenfolge wichtig: Datei 1 zuerst anlegen + NPM neu laden, dann erst Schritt 2 (Compose-Umbau) deployen und in derselben Sitzung Punkt 2 im Advanced-Tab eintragen — sonst zeigt NPM zwischenzeitlich auf den nicht mehr existierenden Container pawfeed.
Schritt 4 — Deploy & Verifikation (20 Min) — ✅ erledigt 2026-08-16
docker compose up -d --buildauf der NAS (/Dockers/PawFeed/docker) — Image 1× gebaut, alle 3 Container gestartet. Kompatibilitäts-Alias (pawfeedaufpawfeed-1) sorgte für nahtlosen Übergang, danach entfernt +pawfeed-1einmal neu erstellt (config-only, kein Rebuild).docker statswährend Lasttest:pawfeed-1104,76%,pawfeed-2102,02%,pawfeed-393,58% — CPU verteilt sich jetzt sauber über alle 3 Prozesse.- Lasttest:
npx autocannon -c 30 -d 10 https://pawfeed.org/— 783 Requests in 10,07s, keine Fehler. - Supabase-Dashboard: Pool Size 15 / Max Client Connections 200 (Shared-Modus) — 3 ×
DB_POOL_MAX=10= 30, unkritisch. - Login/Session-Konsistenz über mehrere Reloads noch nicht gezielt manuell verifiziert (Clerk ist JWT-basiert/stateless, sollte unkritisch sein) — bei Gelegenheit nachholen.
- Mux-Webhook und Stripe-Webhook noch nicht gezielt gegen die neue Replica-Config getestet — bei Gelegenheit nachholen.
Schritt 5 — Rolling-Deploy (Zero-Downtime) — ✅ erledigt 2026-08-16
docker compose up -d --build startete bis hierhin alle 3 Container gleichzeitig neu → kurzes gemeinsames Downtime-Fenster bei jedem Deploy. Behoben mit drei Teilen:
src/app/api/health/route.ts— reiner Liveness-Check (GET→{status:"ok"}), keine DB-/Redis-Abhängigkeit (ein DB-Ausfall darf nicht alle Replicas als "unhealthy" markieren). Insrc/proxy.tssisPublicRouteeingetragen, sonst blockt Clerk den Check.- Docker-
healthcheckindocker-compose.ymlsx-pawfeed-common-Anchor — polltGET http://127.0.0.1:3000/api/healthperwget(einziger HTTP-Client imnode:22-alpine-Runner-Image),interval: 5s,start_period: 15s,retries: 5. docker/rolling-deploy.sh— baut das Image einmal, geht dannpawfeed-1→pawfeed-2→pawfeed-3einzeln durch, wartet nach jedem Neustart bisdocker inspecthealthymeldet (Timeout 60s), bricht vor der nächsten Replica ab, falls eine nicht gesund wird.
Verifiziert: Skript einmal live gegen die Produktionsseite gefahren, parallel jede Sekunde curl auf https://pawfeed.org/ — durchgehend HTTP 200, keine einzige fehlgeschlagene Anfrage während des kompletten Durchlaufs (Build + 3× sequenzieller Neustart).
Deploy-Workflow ab jetzt: ./rolling-deploy.sh statt docker compose up -d --build für Deploys gegen die laufende Seite. start.sh (docker compose up -d --build) bleibt für Erstinstallation bzw. wenn Downtime egal ist.
Risiken / offene Punkte
- DB-Connection-Pool — gelöst:
src/lib/prisma.tscappt jetzt perDB_POOL_MAX(Default/gesetzt: 10). 3 × 10 = 30, weit unter dem verifizierten Supabase-Limit von 200 Max Client Connections. - Rolling-Deploy — gelöst, siehe Schritt 5.
- Host-Port-Mapping — geklärt: NPM erreicht die Replicas per Docker-DNS-Name im
nginx_default-Network (pawfeed-1/2/3:3000), kein Host-Port-Mapping nötig oder vorhanden. - Self-Heal-Fanout-Bug (2026-08-16, gefunden + behoben):
videosRouter.getByPostIds Self-Heal-Pfad (Client pollt PROCESSING, fragt direkt bei Mux nach) setzte Video-Status auf READY, rief aber niefanOutPostauf — Video war abspielbar, landete aber nie im Feed. Kein Replica-Problem, aber durch den Live-Test nach dem Rollout entdeckt. Fix:fanOutPostauch im Self-Heal-Zweig aufrufen (Commitfaf8ee9).