Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 2m21s
Docker Image bauen und veroeffentlichen / lint (push) Successful in 2m27s
Docker Image bauen und veroeffentlichen / test (push) Successful in 3m19s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 7m44s
Ausgerollt am 2026-10-16, Migrationen und Seed beim Start gelaufen. scripts/sponsorship-bootstrap.ts verknuepft den Paket-Honig und legt die Abo-Angebote inaktiv samt Stripe-Preis an. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
216 lines
10 KiB
YAML
216 lines
10 KiB
YAML
# 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-<kurzer Commit> 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-<commit>@sha256:<digest>
|
|
# und hier nachziehen. Tag plus Digest: der Tag ist lesbar, der
|
|
# Digest macht das Image unveraenderlich (Tags lassen sich in der
|
|
# Registry ueberschreiben).
|
|
image: git.encry.de/nick.rosenloecher/wabenhain:sha-ed17d14@sha256:9f3c66f603aaa3d2c7e921b654130cc755b0f84119bd498f9a1a22768e6da6c9
|
|
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 '<', "<!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: "0"
|
|
memory: "0"
|
|
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
|
|
- name: invoices
|
|
persistentVolumeClaim:
|
|
claimName: wabenhain-invoices
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: wabenhain-app
|
|
namespace: wabenhain
|
|
spec:
|
|
selector:
|
|
app: wabenhain-app
|
|
ports:
|
|
- port: 80
|
|
targetPort: 3000
|