# image: unten auf den Build gepinnt, siehe Kommentar dort. 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 das gemountete Volume zusaetzlich der # Gruppe 1001 zuordnet und gruppen-schreibbar macht. # # ACHTUNG, GILT NICHT FUER JEDES BACKEND: fsGroup wirkt nur dort, wo # Kubernetes den Besitz tatsaechlich setzen kann — also bei # blockbasiertem Speicher. Bei SMB/CIFS und NFS wird es IGNORIERT, weil # diese Protokolle keinen per-Datei-Unix-Besitz kennen, den der Knoten # nachtraeglich aendern koennte. Dort gehoert die Zuordnung in die # mountOptions des PersistentVolume: # # mountOptions: # - uid=1001 # - gid=1001 # - dir_mode=0770 # - file_mode=0660 # # Ohne uid/gid erscheint bei SMB ALLES als root:root, und der Container # scheitert trotz fsGroup mit EACCES. Siehe k8s/README.md, Abschnitt # "Uploads lassen sich nicht speichern". securityContext: fsGroup: 1001 runAsNonRoot: true runAsUser: 1001 runAsGroup: 1001 seccompProfile: type: RuntimeDefault containers: - name: app # Fest auf einen Build gepinnt (Tag sha- aus der # Gitea-Pipeline), nie :latest: ein "rollout restart" holt damit # kein ungeprueftes neues Image. Neuer Build: # kubectl -n wabenhain set image deploy/wabenhain-app app=git.encry.de/nick.rosenloecher/wabenhain:sha- # und den Tag hier nachziehen. image: git.encry.de/nick.rosenloecher/wabenhain@sha256:17d880340cdad9448b27a017316f9b906572c455a8e15579ecee50c22e06e14c imagePullPolicy: IfNotPresent securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] 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 # Rechnungsablage, getrennt von den Uploads — auf dieses # Verzeichnis zeigt bewusst KEINE oeffentliche Route. Muss zu # INVOICE_DIR in 01-configmap.yaml passen, ausfuehrliche # Begruendung in 04-uploads-pvc.yaml. - name: invoices mountPath: /app/invoices # 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 # Ohne diesen Eintrag gilt Kubernetes' Vorgabe von EINER Sekunde. # Waehrend ein Admin-Video umgerechnet wird, teilt sich der # Node-Prozess die CPU mit ffmpeg — eine Antwort kann dann laenger # als eine Sekunde brauchen, ohne dass irgendetwas kaputt waere. timeoutSeconds: 5 # 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 # Siehe Begruendung an der Readiness-Probe oben. Hier wiegt es # schwerer: eine fehlgeschlagene LIVENESS-Probe laesst Kubernetes # den Container neu starten. Genau das passierte beim # Admin-Video-Upload, solange die Probe mit der Vorgabe von einer # Sekunde gegen "/" lief — also gegen die Startseite samt # Datenbankabfragen und vollem React-Rendering, waehrend ffmpeg # gleichzeitig die CPU belegte. timeoutSeconds: 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 '<', "