Readiness- und Liveness-Probe zeigten beide auf "/" — eine
force-dynamic-Seite samt Datenbankabfragen und vollem React-Rendering, alle
10 bzw. 30 Sekunden.
Der teure Teil ist dabei nicht das eigentliche Problem. Entscheidend ist,
dass damit auch die LIVENESS-Probe an der Datenbank haengt: faellt die DB
voruebergehend aus, wertet Kubernetes den Container als tot und startet ihn
neu — woraufhin der neue Container laut Dockerfile-CMD zuerst
"prisma migrate deploy" und "prisma db seed" gegen genau die gestoerte
Datenbank faehrt. Aus einer voruebergehenden Stoerung wird so eine
Neustart-Schleife.
Jetzt zwei Endpunkte mit klar getrennter Aufgabe:
- /api/health — nur "Prozess antwortet", ohne Datenbankzugriff.
Liveness-Probe.
- /api/health/ready — zusaetzlich "SELECT 1" gegen die Datenbank, sonst 503.
Readiness-Probe und Docker-Healthcheck.
Faellt die Readiness-Probe aus, nimmt Kubernetes den Pod nur aus dem
Load-Balancing und startet ihn nicht neu — genau das richtige Verhalten.
"SELECT 1" laeuft unveraendert auf PostgreSQL und MySQL/MariaDB und
beruehrt keine Tabelle, ist also unabhaengig vom Migrationsstand. Die
Fehlerdetails gehen nur ins Server-Log, nicht in die oeffentliche Antwort.
docker-compose.yml bekommt denselben Readiness-Endpunkt als Healthcheck,
mit start_period fuer den Migrations-/Seed-Lauf vor dem Serverstart.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
133 lines
5.4 KiB
YAML
133 lines
5.4 KiB
YAML
services:
|
|
db:
|
|
image: postgres:16.15-alpine
|
|
restart: unless-stopped
|
|
# Nur relevant, wenn DATABASE_PROVIDER=postgresql (Standard) — bei mysql
|
|
# stattdessen "docker compose --profile mysql up -d db-mysql" starten,
|
|
# siehe README, Abschnitt "Datenbank-Provider wechseln".
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-wabenhain}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD muss in .env gesetzt sein}
|
|
POSTGRES_DB: ${POSTGRES_DB:-wabenhain}
|
|
ports:
|
|
# Nur an localhost binden statt an alle Interfaces — die App erreicht
|
|
# die DB ueber das interne Compose-Netzwerk, dieser Port dient nur
|
|
# lokalem Debugging (z.B. per psql/DB-GUI vom selben Host aus).
|
|
- "127.0.0.1:5432:5432"
|
|
volumes:
|
|
- wabenhain_db_data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-wabenhain}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
# Alternative zu "db": nur starten, wenn DATABASE_PROVIDER=mysql in .env
|
|
# gesetzt ist ("docker compose --profile mysql up -d db-mysql"). Laeuft
|
|
# nicht automatisch mit "docker compose up" mit, um den Postgres-Standardfall
|
|
# nicht zu verlangsamen.
|
|
db-mysql:
|
|
image: mysql:8.4.11
|
|
profiles: ["mysql"]
|
|
restart: unless-stopped
|
|
environment:
|
|
MYSQL_USER: ${MYSQL_USER:-wabenhain}
|
|
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?MYSQL_PASSWORD muss in .env gesetzt sein, falls DATABASE_PROVIDER=mysql}
|
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-wabenhain}
|
|
MYSQL_ALLOW_EMPTY_PASSWORD: "no"
|
|
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
|
|
ports:
|
|
- "127.0.0.1:3306:3306"
|
|
volumes:
|
|
- wabenhain_db_mysql_data:/var/lib/mysql
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u${MYSQL_USER:-wabenhain} -p${MYSQL_PASSWORD}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
|
|
# Einziger nach aussen exponierter Dienst: terminiert die Client-Verbindung
|
|
# und setzt X-Forwarded-For dabei neu (verwirft einen vom Client selbst
|
|
# mitgeschickten Wert, siehe Caddyfile-Kommentar). Ohne diesen Proxy koennte
|
|
# "app" direkt erreicht werden und jedes Rate-Limit (Login, 2FA, Checkout)
|
|
# liesse sich durch einen gefaelschten X-Forwarded-For-Header umgehen.
|
|
proxy:
|
|
image: caddy:2.11.4-alpine
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- app
|
|
ports:
|
|
- "80:80"
|
|
# Fuer echtes HTTPS mit eigener Domain zusaetzlich freigeben und
|
|
# SITE_ADDRESS in .env auf die Domain setzen (siehe Caddyfile):
|
|
# - "443:443"
|
|
# - "443:443/udp"
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- wabenhain_caddy_data:/data
|
|
environment:
|
|
SITE_ADDRESS: ${SITE_ADDRESS:-}
|
|
|
|
app:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
restart: unless-stopped
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
required: false
|
|
expose:
|
|
- "3000"
|
|
# Prueft denselben Endpunkt wie die Kubernetes-Readiness-Probe (siehe
|
|
# k8s/05-app-deployment.yaml): nicht nur "Prozess laeuft", sondern "App
|
|
# erreicht die Datenbank". start_period deckt den Migrations-/Seed-Lauf
|
|
# ab, den der Container laut Dockerfile-CMD vor dem Serverstart ausfuehrt.
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "node -e \"fetch('http://127.0.0.1:3000/api/health/ready').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 90s
|
|
volumes:
|
|
# Persistiert hochgeladene Admin-Bilder/-Videos (Dockerfile legt
|
|
# /app/uploads bereits mit passendem Besitzer fuer den non-root
|
|
# "nextjs"-User an) ueber Container-Neustarts/-Redeploys hinweg — ohne
|
|
# dieses Volume waere jeder Upload nach dem naechsten "docker compose
|
|
# up" wieder weg.
|
|
- wabenhain_uploads_data:/app/uploads
|
|
environment:
|
|
# Muss zum Volume-Mount oben passen — siehe .env.example.
|
|
UPLOAD_DIR: /app/uploads
|
|
# DATABASE_URL kommt komplett aus .env — je nach DATABASE_PROVIDER
|
|
# entweder die Postgres- ("db"-Service, Port 5432) oder die
|
|
# MySQL-Verbindungszeichenkette ("db-mysql"-Service, Port 3306).
|
|
DATABASE_PROVIDER: ${DATABASE_PROVIDER:-postgresql}
|
|
DATABASE_URL: ${DATABASE_URL}
|
|
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
|
|
STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUBLISHABLE_KEY}
|
|
STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}
|
|
# Nur fuer den Seed-Schritt relevant (legt das initiale Admin-Konto an,
|
|
# siehe README "Admin-Zugang") — kein Basic-Auth-Geheimnis mehr. Bewusst
|
|
# OHNE Default-Wert: ein vorhersehbarer Wert wie "[email protected]"
|
|
# liesse sich vor dem ersten Deploy-Seed-Lauf selbst registrieren, siehe
|
|
# prisma/seed.ts fuer die zusaetzliche Absicherung dagegen.
|
|
ADMIN_EMAIL: ${ADMIN_EMAIL:?ADMIN_EMAIL muss in .env gesetzt sein}
|
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD muss in .env gesetzt sein}
|
|
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET muss in .env gesetzt sein}
|
|
GOOGLE_PLACES_API_KEY: ${GOOGLE_PLACES_API_KEY}
|
|
GOOGLE_PLACE_ID: ${GOOGLE_PLACE_ID}
|
|
SMTP_HOST: ${SMTP_HOST}
|
|
SMTP_PORT: ${SMTP_PORT:-587}
|
|
SMTP_USER: ${SMTP_USER}
|
|
SMTP_PASSWORD: ${SMTP_PASSWORD}
|
|
SMTP_FROM: ${SMTP_FROM:-Wabenhain <[email protected]>}
|
|
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
|
|
NODE_ENV: production
|
|
|
|
volumes:
|
|
wabenhain_db_data:
|
|
wabenhain_db_mysql_data:
|
|
wabenhain_caddy_data:
|
|
wabenhain_uploads_data:
|