Files
Wabenhain/k8s/05-app-deployment.yaml
T
Nick SzaunigandClaude Opus 5 a78c1cd235 fix: hochgeladene Bilder ueberleben einen Redeploy
Das uploads-Volume im App-Deployment war ein emptyDir-Platzhalter. Der ist
an die Lebensdauer des Pods gebunden: bei jedem Neustart, jedem Redeploy
und jeder Node-Verlagerung waren saemtliche hochgeladenen Produkt-,
Ernten- und Inhaltsfotos weg. Die Datenbank behaelt dabei ihre Bildpfade —
die Seite haette danach ueberall kaputte Bilder gezeigt, ohne dass
irgendwo ein Fehler aufgetaucht waere.

Neu: k8s/04-uploads-pvc.yaml (die Nummer war in der Reihenfolge noch frei)
mit einem PersistentVolumeClaim, den das Deployment jetzt bindet.
storageClassName ist bewusst auskommentiert, damit das Manifest ohne
Aenderung gegen die Default-StorageClass des Clusters anwendbar bleibt.

Damit einher geht zwingend "strategy: Recreate": Kubernetes' Standard
"RollingUpdate" startet den neuen Pod, BEVOR der alte beendet ist. Ein
ReadWriteOnce-Claim kann aber nur an einen Pod gebunden sein — der neue
Pod bliebe mit "Multi-Attach error for volume" in ContainerCreating
haengen, und das im k8s/README.md dokumentierte
"kubectl rollout restart" liefe schlicht in den Timeout. Recreate kostet
wenige Sekunden Ausfall pro Redeploy und sorgt nebenbei dafuer, dass nie
zwei Pods gleichzeitig laufen, die sich sonst die im Prozessspeicher
gehaltenen Rate-Limits teilen wuerden.

Alle Manifeste gegen einen YAML-Parser geprueft, zusaetzlich strukturell:
claimName trifft den PVC-Namen, PVC liegt im selben Namespace, und der
mountPath stimmt mit UPLOAD_DIR aus 01-configmap.yaml ueberein.

preflight: NEXT_PUBLIC_SITE_URL wird jetzt auch gegen "example.com"
geprueft. Die ausgelieferte Vorgabe ist "shop.example.com" — blieb sie
stehen, zeigten Sitemap, Canonicals und jeder Link in jeder
Bestellbestaetigung auf eine fremde Domain, ohne dass die bisherigen
HTTPS-/localhost-Pruefungen angeschlagen haetten.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-15 10:13:06 +02:00

160 lines
7.6 KiB
YAML

# image: unten mit der tatsaechlichen Registry/Tag aus der Gitea-Actions-
# Pipeline ersetzen. Kein separater Migrations-Job mehr noetig: der
# Container fuehrt "prisma migrate deploy" + "prisma db seed" selbst aus,
# BEVOR er den Server startet (siehe Dockerfile-CMD in der runner-Stage) —
# beide Schritte sind idempotent, laufen also bei jedem Neustart gefahrlos
# erneut. Readiness-/Liveness-Probes greifen automatisch erst, sobald Port
# 3000 offen ist, also nach abgeschlossener Migration/Seed.
apiVersion: apps/v1
kind: Deployment
metadata:
name: wabenhain-app
namespace: wabenhain
spec:
# ACHTUNG bevor du hochskalierst: lib/rate-limit.ts haelt Login-/2FA-/
# Checkout-Limits aktuell im Prozessspeicher, nicht in einem gemeinsamen
# Store. Mit replicas > 1 verteilt sich derselbe Nutzer per Load-Balancing
# auf mehrere Prozesse mit je eigenem Zaehler — das Limit wird effektiv um
# den Skalierungsfaktor schwaecher (2 Replicas = 2x mehr Versuche moeglich),
# ohne dass das im Frontend sichtbar waere. Fuer replicas > 1 zuerst
# lib/rate-limit.ts auf einen gemeinsamen Store (z.B. Redis) umstellen.
replicas: 1
# PFLICHT in Verbindung mit dem uploads-Volume (siehe 04-uploads-pvc.yaml):
# die Standardstrategie "RollingUpdate" startet den NEUEN Pod, bevor der
# alte beendet ist. Ein ReadWriteOnce-Volume kann aber immer nur an einen
# Pod gebunden sein — der neue Pod bliebe in ContainerCreating mit
# "Multi-Attach error for volume" haengen, und ein
# "kubectl rollout restart" (der im k8s/README.md dokumentierte Weg fuer
# ein neues Image) liefe schlicht in den Timeout.
#
# "Recreate" beendet den alten Pod zuerst. Das kostet waehrend des
# Redeploys ein paar Sekunden Ausfall — bei einer Replica ist der
# Unterschied ohnehin gering, und der Container migriert beim Start erst
# die Datenbank, bevor er Verkehr annimmt. Der Nebeneffekt ist erwuenscht:
# es laufen nie zwei Pods gleichzeitig, die sich sonst die im
# Prozessspeicher gehaltenen Rate-Limits teilen wuerden (siehe Kommentar
# zu replicas oben).
strategy:
type: Recreate
selector:
matchLabels:
app: wabenhain-app
template:
metadata:
labels:
app: wabenhain-app
spec:
# fsGroup=1001 muss zur "nodejs"-Gruppe (gid 1001) passen, die das
# Dockerfile fuer den non-root "nextjs"-Laufzeitbenutzer anlegt: ohne
# dieses Feld mountet Kubernetes das "uploads"-Volume (egal ob emptyDir
# oder ein echtes PVC) mit root:root-Besitz — das ueberschreibt den im
# Image bereits per chown gesetzten Besitzer von /app/uploads
# vollstaendig, und der nicht-root laufende Container koennte trotzdem
# keine Datei dort anlegen (EACCES bei jedem Admin-Upload). fsGroup
# sorgt dafuer, dass Kubernetes JEDES gemountete Volume zusaetzlich der
# Gruppe 1001 zuordnet und gruppen-schreibbar macht.
securityContext:
fsGroup: 1001
containers:
- name: app
image: git.encry.de/nick.rosenloecher/wabenhain:latest
ports:
- containerPort: 3000
envFrom:
- configMapRef:
name: wabenhain-config
- secretRef:
name: wabenhain-secrets
volumeMounts:
# Muss auf denselben Pfad zeigen wie UPLOAD_DIR in
# 01-configmap.yaml. Das "uploads"-Volume selbst wird HIER
# bewusst nicht definiert (kein PV/PVC in diesem Manifest) — das
# haengt vom tatsaechlichen Storage-Backend ab (SMB/NFS/
# hostPath/...), das die Betreiberin bereitstellt. Unten im
# "volumes:"-Block einen passenden Eintrag mit demselben Namen
# ("uploads") ergaenzen, z.B. ueber eine
# persistentVolumeClaim.claimName, sonst startet der Pod nicht
# (fehlende Volume-Referenz).
- name: uploads
mountPath: /app/uploads
# Prueft die Datenbank mit (siehe app/api/health/ready/route.ts).
# Schlaegt sie fehl, nimmt Kubernetes den Pod nur aus dem
# Load-Balancing, startet ihn aber nicht neu.
readinessProbe:
httpGet:
path: /api/health/ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 10
# Bewusst der DB-FREIE Endpunkt (siehe app/api/health/route.ts):
# eine Liveness-Probe an der Datenbank wuerde aus einer
# voruebergehenden DB-Stoerung eine Neustart-Schleife machen, weil
# der neu startende Container laut Dockerfile-CMD zuerst
# "prisma migrate deploy" gegen dieselbe gestoerte Datenbank faehrt.
# Beide Probes zeigten vorher auf "/" — also auf eine
# force-dynamic-Seite mit DB-Abfragen und vollem React-Rendering
# alle 10 bzw. 30 Sekunden.
livenessProbe:
httpGet:
path: /api/health
port: 3000
# initialDelaySeconds/failureThreshold grosszuegiger als sonst
# ueblich: der Container migriert/seedet die DB selbst vor dem
# Serverstart (siehe Kommentar oben) — genug Spielraum, damit ein
# laengerer erster Migrationslauf nicht faelschlich als haengender
# Prozess gewertet und der Container neu gestartet wird.
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 5
# Bemessen nach dem teuersten Vorgang der Anwendung, dem
# Admin-Video-Upload: Node laedt den multipart-Body beim
# request.formData() vollstaendig in den Speicher, und die Datei
# darf bis zu 100 MB gross sein (MAX_UPLOAD_VIDEO_BYTES in
# lib/uploads.ts). Zusammen mit dem Grundverbrauch des
# Next.js-Servers sprengte das ein Limit von 512Mi — der Kernel
# beendet den Prozess dann mitten im Upload (OOMKilled), und im
# Browser kommt davon nur die HTML-Fehlerseite des Reverse-Proxy an
# ("Unexpected token '<', "<!DOCTYPE "... is not valid JSON").
#
# Die Route selbst haelt inzwischen nur noch den Body im Speicher
# und streamt alles Weitere ueber die Platte (siehe
# app/api/admin/upload-video/route.ts) — 1Gi gibt darueber hinaus
# Reserve, damit ein zweiter gleichzeitiger Zugriff den Pod nicht
# kippt.
#
# ffmpeg ist CPU-gebunden: mit nur 100m dauert das Umrechnen ein
# Vielfaches und laeuft eher in ein Proxy-Zeitlimit. Bewusst KEIN
# CPU-Limit, nur ein hoeherer Request — ein Limit wuerde den
# Transcode hart drosseln, auch wenn der Knoten gerade frei ist.
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
memory: 1Gi
volumes:
# Bindet den PersistentVolumeClaim aus 04-uploads-pvc.yaml — dieses
# Manifest MUSS vor dem Deployment angewendet werden, sonst bleibt
# der Pod in "Pending" (unaufloesbare Volume-Referenz).
#
# Vorher stand hier ein emptyDir-Platzhalter. Das ist kein
# persistenter Speicher: bei jedem Pod-Neustart, jedem Redeploy und
# jeder Node-Verlagerung waren saemtliche hochgeladenen Produkt- und
# Erntenfotos weg, waehrend die Datenbank ihre Pfade behielt — die
# Seite zeigte danach ueberall kaputte Bilder, ohne Fehlermeldung.
- name: uploads
persistentVolumeClaim:
claimName: wabenhain-uploads
---
apiVersion: v1
kind: Service
metadata:
name: wabenhain-app
namespace: wabenhain
spec:
selector:
app: wabenhain-app
ports:
- port: 80
targetPort: 3000