Next.js standalone is single-process, so one container only used one CPU core. A load test hit a hard ceiling at ~60-65 req/sec with one core pegged at 133% while 3 cores sat idle. Now runs as pawfeed-1/2/3 behind an NPM least_conn upstream, verified via load test showing CPU spread evenly across all three replicas. Also caps the Prisma pg pool per-replica via DB_POOL_MAX so 3 replicas stay well under the Supabase pooler's connection limit.
7.4 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.
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 fehlt noch:
docker compose up --buildstartet weiterhin alle 3 Container gleichzeitig neu → kurzes gemeinsames Downtime-Fenster bei jedem künftigen Deploy. Echter Zero-Downtime-Rolling-Deploy (Container einzeln nacheinander mit--no-depsneu bauen) ist ein sinnvoller Folgeschritt, aber nicht Teil dieser Roadmap. - 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.