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]>
5.4 KiB
Wabenhain in Kubernetes
Rohe Manifeste, mit kubectl apply -f k8s/<datei> einzeln oder als Ordner
anwendbar. Nummerierung = empfohlene Reihenfolge.
Einmalige Vorbereitung
-
Gitea Actions:
.gitea/workflows/docker-build.ymlbaut bei jedem Push aufmaindas Image<registry>/<owner>/wabenhain:latest(+:sha-<kurzhash>,:vX.Y.Zbei Tags) in der Gitea-Container-Registry. Ein einzelnes Image reicht fuer05-app-deployment.yaml— es traegt sowohl die Next.js-App als auch die Prisma-CLI/tsx und migriert/seedet die Datenbank beim Start selbst (siehe Punkt 2 unten), kein separates Migrations-Image mehr.Voraussetzung: ein Repo-/Org-Secret
REGISTRY_TOKENin Gitea anlegen (Personal Access Token mitpackage-Scope, unter Benutzereinstellungen → Anwendungen → Access Tokens erzeugen, dann unter Repo-Einstellungen → Actions → Secrets alsREGISTRY_TOKENhinterlegen). Der automatisch von Gitea Actions bereitgestellteGITEA_TOKENreicht dafuer aktuell NICHT aus (fehlendes Package-Write-Recht, Stand heute). -
Ein Docker-Image ist Provider-uebergreifend: Prisma generiert den Client Provider-spezifisch — ein
.next/standalone-Build kann seinen generierten Client nicht zur Laufzeit wechseln. Deshalb generiertscripts/generate-prisma-clients.mjs(Teil vonpnpm build, laeuft also automatisch bei jedem Image-Build) BEIDE Varianten ins selbe Image; die Wahl zwischen ihnen passiert zur Laufzeit ueberDATABASE_PROVIDER, siehesrc/lib/prisma.ts. Ein Image reicht also fuer Postgres UND MySQL — nurDATABASE_PROVIDER/DATABASE_URLin01-configmap.yaml/02-secret.yamlmuessen zur tatsaechlich gewaehlten Datenbank passen. Beim Container-Start fuehrt derselbe Prozess erstprisma migrate deployundprisma db seedaus, BEVOR der Next.js-Server startet (siehe Dockerfile-CMD in derrunner-Stage) — beide Schritte sind idempotent und bei jedem Neustart gefahrlos, kein separater Migrations-Job noetig. -
k8s/02-secret.example.yamlnachk8s/02-secret.yamlkopieren (per.gitignoreausgeschlossen — NIE mit echten Werten committen) und ausfuellen. Alternativ direkt perkubectl create secret, siehe Kommentar in der Beispieldatei. -
01-configmap.yaml:DATABASE_PROVIDER(postgresql/mysql) undNEXT_PUBLIC_SITE_URL(echte Domain) anpassen. -
04-uploads-pvc.yaml:storageClassNameauf eine im Cluster vorhandene StorageClass setzen (kubectl get storageclass; die Zeile ist auskommentiert, dann greift die als(default)markierte) und die Groesse pruefen. Ohne diesen Claim gibt es keinen dauerhaften Speicher fuer Produkt- und Erntenfotos. -
image:in05-app-deployment.yamlzeigt bereits aufgit.encry.de/nick.rosenloecher/wabenhain— nur noch den Tag pruefen (:latestpasst fuer den jeweils neuestenmain-Build). In06-ingress.yamlshop.example.comdurch die echte Domain ersetzen (zweimal:host:undtls.hosts).
Deployment
kubectl apply -f k8s/00-namespace.yaml
kubectl apply -f k8s/01-configmap.yaml
kubectl apply -f k8s/02-secret.yaml
# Nur EINE der beiden Datenbanken anwenden, passend zu DATABASE_PROVIDER:
kubectl apply -f k8s/03-postgres.yaml
# kubectl apply -f k8s/03-mysql.yaml
kubectl wait --for=condition=ready pod -l app=wabenhain-postgres -n wabenhain --timeout=120s
# Persistenter Speicher fuer hochgeladene Bilder — MUSS vor dem Deployment
# angewendet werden, sonst bleibt der App-Pod in "Pending" haengen.
kubectl apply -f k8s/04-uploads-pvc.yaml
kubectl apply -f k8s/05-app-deployment.yaml
kubectl apply -f k8s/06-ingress.yaml
Migrationen + Seed (nur Admin-Konto, KEINE Beispieldaten — siehe Punkt 2
oben) laufen automatisch beim Container-Start —
kubectl rollout status deployment/wabenhain-app -n wabenhain zeigt, wann
der erste Pod bereit ist.
Bei einem neuen Image (Redeploy)
kubectl rollout restart deployment/wabenhain-app -n wabenhain
Migrationen + Seed laufen automatisch erneut beim Start jedes neuen Pods (idempotent, siehe oben).
Das Deployment nutzt dabei die Strategie Recreate, nicht das
Kubernetes-Standardverhalten RollingUpdate: der alte Pod wird zuerst
beendet, dann der neue gestartet. Das ist mit dem uploads-Volume zwingend —
ein ReadWriteOnce-Claim kann immer nur an einen Pod gebunden sein, ein
parallel hochfahrender neuer Pod bliebe mit
Multi-Attach error for volume in ContainerCreating haengen und der
Rollout liefe in den Timeout. Der Preis sind wenige Sekunden Ausfall pro
Redeploy.
Bekannte Grenzen dieser Manifeste
replicas: 1in05-app-deployment.yaml, bewusst nicht mehr: das Login-/2FA-/Checkout-Rate-Limiting (lib/rate-limit.ts) haelt seinen Zustand im Prozessspeicher, nicht in einem gemeinsamen Store. Mehrere Replicas wuerden das Limit unbemerkt proportional aufweichen. Fuer horizontale Skalierung zuerstlib/rate-limit.tsauf Redis o.ae. umstellen.- Kein
HorizontalPodAutoscaler, keinPodDisruptionBudget, keineNetworkPolicy— je nach Cluster/Anforderungen ergaenzen. - Die Postgres-/MySQL-StatefulSets sind Single-Instance ohne Replikation/
Backup-Automation. Fuer produktiven Betrieb einen Managed-Datenbank-Dienst
oder einen dedizierten DB-Operator (z.B. CloudNativePG) in Erwaegung
ziehen —
DATABASE_URLin02-secret.yamleinfach auf den externen Endpunkt zeigen lassen,03-postgres.yaml/03-mysql.yamldann nicht anwenden.