# Wabenhain in Kubernetes Rohe Manifeste, mit `kubectl apply -f k8s/` einzeln oder als Ordner anwendbar. Nummerierung = empfohlene Reihenfolge. ## Einmalige Vorbereitung 1. **Gitea Actions**: `.gitea/workflows/docker-build.yml` baut bei jedem Push auf `main` das Image `//wabenhain:latest` (+ `:sha-`, `:vX.Y.Z` bei Tags) in der Gitea-Container-Registry. Ein einzelnes Image reicht fuer `05-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_TOKEN` in Gitea anlegen (Personal Access Token mit `package`-Scope, unter Benutzereinstellungen → Anwendungen → Access Tokens erzeugen, dann unter Repo-Einstellungen → Actions → Secrets als `REGISTRY_TOKEN` hinterlegen). Der automatisch von Gitea Actions bereitgestellte `GITEA_TOKEN` reicht dafuer aktuell NICHT aus (fehlendes Package-Write-Recht, Stand heute). 2. **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 generiert `scripts/generate-prisma-clients.mjs` (Teil von `pnpm build`, laeuft also automatisch bei jedem Image-Build) BEIDE Varianten ins selbe Image; die Wahl zwischen ihnen passiert zur Laufzeit ueber `DATABASE_PROVIDER`, siehe `src/lib/prisma.ts`. Ein Image reicht also fuer Postgres UND MySQL — nur `DATABASE_PROVIDER`/`DATABASE_URL` in `01-configmap.yaml`/ `02-secret.yaml` muessen zur tatsaechlich gewaehlten Datenbank passen. Beim Container-Start fuehrt derselbe Prozess erst `prisma migrate deploy` und `prisma db seed` aus, BEVOR der Next.js-Server startet (siehe Dockerfile-CMD in der `runner`-Stage) — beide Schritte sind idempotent und bei jedem Neustart gefahrlos, kein separater Migrations-Job noetig. 3. `k8s/02-secret.example.yaml` nach `k8s/02-secret.yaml` kopieren (per `.gitignore` ausgeschlossen — NIE mit echten Werten committen) und ausfuellen. Alternativ direkt per `kubectl create secret`, siehe Kommentar in der Beispieldatei. 4. `01-configmap.yaml`: `DATABASE_PROVIDER` (postgresql/mysql) und `NEXT_PUBLIC_SITE_URL` (echte Domain) anpassen. 5. `04-uploads-pvc.yaml`: `storageClassName` auf 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. 6. `image:` in `05-app-deployment.yaml` zeigt bereits auf `git.encry.de/nick.rosenloecher/wabenhain` — nur noch den Tag pruefen (`:latest` passt fuer den jeweils neuesten `main`-Build). In `06-ingress.yaml` `shop.example.com` durch die echte Domain ersetzen (zweimal: `host:` und `tls.hosts`). ## Deployment ```bash 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 # Taegliche Datenbanksicherung (siehe "Sicherung und Wiederherstellung"). kubectl apply -f k8s/07-backup-cronjob.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) ```bash 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: 1`** in `05-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 zuerst `lib/rate-limit.ts` auf Redis o.ae. umstellen. - Kein `HorizontalPodAutoscaler`, kein `PodDisruptionBudget`, keine `NetworkPolicy` — je nach Cluster/Anforderungen ergaenzen. - Die Postgres-/MySQL-StatefulSets sind Single-Instance **ohne Replikation**. Eine taegliche Sicherung gibt es inzwischen (`07-backup-cronjob.yaml`, siehe "Sicherung und Wiederherstellung"), sie liegt aber im selben Cluster und ersetzt damit weder Replikation noch eine Auslagerung an einen anderen Ort. Faellt der Knoten aus, ist der Shop bis zur Wiederherstellung offline. Fuer produktiven Betrieb mit hoeherem Anspruch einen Managed-Datenbank-Dienst oder einen dedizierten DB-Operator (z.B. CloudNativePG) in Erwaegung ziehen — `DATABASE_URL` in `02-secret.yaml` einfach auf den externen Endpunkt zeigen lassen, `03-postgres.yaml`/`03-mysql.yaml` dann nicht anwenden. ## Sicherung und Wiederherstellung `07-backup-cronjob.yaml` legt jede Nacht um 03:15 Uhr eine gzip-komprimierte `pg_dump`-Sicherung auf einem eigenen Volume ab und haelt sie 30 Tage vor. Ohne diese Sicherung liegen Bestellungen, Rechnungen und Kundenkonten in einem einzelnen PersistentVolume ohne jedes Netz — und Rechnungen unterliegen in Deutschland einer zehnjaehrigen Aufbewahrungspflicht (§ 147 AO). **Sicherungen ansehen:** ```bash kubectl get cronjob wabenhain-db-backup -n wabenhain kubectl logs -n wabenhain -l job-name --tail=50 # Ausgabe des letzten Laufs ``` **Sofort eine Sicherung ausloesen** (etwa vor einem riskanten Eingriff): ```bash kubectl create job --from=cronjob/wabenhain-db-backup manuell-$(date +%s) -n wabenhain ``` **Aus dem Cluster herausziehen.** Die Sicherung im Cluster schuetzt gegen Volume-Verlust und Bedienfehler, nicht gegen den Ausfall des gesamten Clusters oder Standorts. Deshalb regelmaessig (oder per eigenem Cron auf einem Rechner ausserhalb) kopieren: ```bash POD=$(kubectl get pod -n wabenhain -l job-name -o name | head -1) kubectl exec -n wabenhain "$POD" -- ls -1 /backup kubectl cp wabenhain/"${POD#pod/}":/backup/wabenhain-2026-01-31_0315.sql.gz ./wabenhain-2026-01-31.sql.gz ``` **Wiederherstellen.** Vorher die App anhalten, damit waehrend des Einspielens niemand schreibt — `--clean --if-exists` im Dump loescht die vorhandenen Tabellen zuerst: ```bash kubectl scale deployment/wabenhain-app -n wabenhain --replicas=0 gunzip -c wabenhain-2026-01-31.sql.gz | kubectl exec -i -n wabenhain wabenhain-postgres-0 -- psql -U wabenhain -d wabenhain kubectl scale deployment/wabenhain-app -n wabenhain --replicas=1 ``` Der App-Container fuehrt beim naechsten Start ohnehin `prisma migrate deploy` aus (siehe Dockerfile-CMD) — ein Dump aus einem aelteren Schemastand wird dadurch automatisch nachgezogen. **Eine ungepruefte Sicherung ist keine Sicherung.** Einmal nach der Einrichtung und danach gelegentlich einen Wiederherstellungsversuch gegen eine Wegwerf-Datenbank fahren, statt sich erst im Ernstfall darauf zu verlassen.