docs/EINRICHTUNG.md walks from nothing to a running shop: tools and access, generating database/session/admin values, the Entra ID app registration used for both sign-in and mail (redirect URI, client secret, SMTP.SendAsApp with admin consent, enterprise app object id), unlocking the shared mailbox in Exchange Online, Stripe keys and webhook events, optional services, a complete secret/config map table, rollout, verification, maintenance (secret expiry) and troubleshooting keyed to the messages of the admin email test. Linked from README and k8s/README. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
388 lines
17 KiB
Markdown
388 lines
17 KiB
Markdown
# Wabenhain in Kubernetes
|
||
|
||
Rohe Manifeste, mit `kubectl apply -f k8s/<datei>` einzeln oder als Ordner
|
||
anwendbar. Nummerierung = empfohlene Reihenfolge.
|
||
|
||
Woher jeder Wert in `02-secret.yaml` und `01-configmap.yaml` kommt (Entra ID,
|
||
Microsoft 365, Stripe, optionale Dienste) und wie man danach prueft, dass
|
||
alles laeuft: [../docs/EINRICHTUNG.md](../docs/EINRICHTUNG.md).
|
||
|
||
## Einmalige Vorbereitung
|
||
|
||
1. **Gitea Actions**: `.gitea/workflows/docker-build.yml` baut bei jedem
|
||
Push auf `main` das Image `<registry>/<owner>/wabenhain:latest` (+
|
||
`:sha-<kurzhash>`, `: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). Die Domain steht
|
||
in `06-ingress.yaml` (`host:` und `tls.hosts`) und in `01-configmap.yaml`
|
||
(`NEXT_PUBLIC_SITE_URL`) bereits als `wabenhain.com` — beide Stellen
|
||
muessen uebereinstimmen, sonst zeigen Sitemap, Canonical-Tags und die
|
||
Links in allen Bestellbestaetigungen woandershin.
|
||
|
||
## 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.
|
||
|
||
## Bilder/Videos verschwinden nach jedem Redeploy
|
||
|
||
Symptom: nach einem Redeploy sind alle hochgeladenen Produkt-, Ernten- und
|
||
Inhaltsbilder weg und muessen neu hochgeladen werden.
|
||
|
||
Ursache: das Volume `uploads` im App-Deployment ist noch ein `emptyDir`.
|
||
Das ist an die Lebensdauer des Pods gebunden — jeder Neustart loescht es.
|
||
Die Datenbank behaelt dabei ihre Bildpfade, die Dateien dahinter sind aber
|
||
fort; im Shop erscheinen deshalb leere Flaechen statt einer Fehlermeldung.
|
||
|
||
Wichtig: Die Gitea-Pipeline baut nur das Image. Sie wendet **keine**
|
||
Kubernetes-Manifeste an. Ein `kubectl rollout restart` holt zwar das neue
|
||
Image, laesst die Deployment-Definition im Cluster aber unveraendert — die
|
||
Korrektur unten muss deshalb einmal von Hand angewendet werden:
|
||
|
||
```bash
|
||
# 1. Aktuellen Zustand pruefen — steht hier "emptyDir", ist genau das die Ursache:
|
||
kubectl get deployment wabenhain-app -n wabenhain -o jsonpath='{.spec.template.spec.volumes[?(@.name=="uploads")]}{"
|
||
"}'
|
||
|
||
# 2. Dauerhaften Speicher anlegen und Deployment aktualisieren:
|
||
kubectl apply -f k8s/04-uploads-pvc.yaml
|
||
kubectl apply -f k8s/05-app-deployment.yaml
|
||
|
||
# 3. Gegenprobe — jetzt muss dort ein persistentVolumeClaim stehen:
|
||
kubectl get deployment wabenhain-app -n wabenhain -o jsonpath='{.spec.template.spec.volumes[?(@.name=="uploads")]}{"
|
||
"}'
|
||
kubectl get pvc wabenhain-uploads -n wabenhain
|
||
```
|
||
|
||
Danach ueberstehen Uploads jeden Redeploy. Bereits verlorene Dateien lassen
|
||
sich nicht wiederherstellen — sie muessen einmalig neu hochgeladen werden.
|
||
|
||
Das Deployment nutzt dabei `strategy: Recreate` (siehe Abschnitt oben):
|
||
ohne das bliebe der neue Pod beim Redeploy an einem
|
||
`Multi-Attach error for volume` haengen, weil ein
|
||
`ReadWriteOnce`-Claim nur an einen Pod gebunden sein kann.
|
||
|
||
**Kontrolle im laufenden Betrieb:** Das Admin-Dashboard gleicht bei jedem
|
||
Aufruf ab, ob die in der Datenbank hinterlegten Bild-/Videopfade als Dateien
|
||
vorliegen, und zeigt sonst eine Warnung mit Anzahl und Beispielpfaden
|
||
(`lib/upload-health.ts`). Dieser Fall faellt damit sofort auf, statt erst
|
||
dann, wenn Kundschaft im Shop leere Flaechen sieht.
|
||
|
||
## Server startet beim Video-Upload neu
|
||
|
||
Symptom: Ein Video im Admin hochladen, und der Pod startet mittendrin neu. Kleine
|
||
Bilder gehen problemlos durch.
|
||
|
||
Ursache ist fast immer, dass im Cluster noch eine ältere, von Hand gepflegte
|
||
Fassung des Deployments läuft und nicht die aus diesem Verzeichnis. Zuerst den
|
||
Ist-Zustand feststellen:
|
||
|
||
```bash
|
||
# Abbruchgrund des letzten Containerlaufs:
|
||
kubectl -n wabenhain get pods -l app=wabenhain-app \
|
||
-o jsonpath='{.items[0].status.containerStatuses[0].lastState.terminated.reason}{"\n"}'
|
||
|
||
# Die drei Werte, auf die es ankommt:
|
||
kubectl -n wabenhain get deploy wabenhain-app \
|
||
-o jsonpath='limit={.spec.template.spec.containers[0].resources.limits.memory}{"\n"}liveness={.spec.template.spec.containers[0].livenessProbe.httpGet.path}{"\n"}timeout={.spec.template.spec.containers[0].livenessProbe.timeoutSeconds}{"\n"}'
|
||
```
|
||
|
||
Erwartet wird `limit=1Gi`, `liveness=/api/health`, `timeout=5`. Steht dort
|
||
`512Mi`, `/` oder ein leerer Timeout, ist genau das die Ursache — es gibt zwei
|
||
Wege, wie das den Container umbringt:
|
||
|
||
- **`liveness=/`**: Die Startseite ist `force-dynamic`, fragt die Datenbank ab
|
||
und rendert React vollständig. Ohne eigenen `timeoutSeconds` gilt Kubernetes'
|
||
Vorgabe von **einer Sekunde**. Während ffmpeg das Video umrechnet, teilt sich
|
||
der Node-Prozess die CPU — die Seite antwortet nicht rechtzeitig, die Probe
|
||
schlägt fehl, und nach `failureThreshold × periodSeconds` startet kubelet den
|
||
Container neu. `/api/health` ist stattdessen datenbankfrei und billig.
|
||
- **`limit=512Mi`**: Node-Server und ffmpeg teilen sich dasselbe Speicherbudget.
|
||
Bei einem 4K-Video reicht das nicht, der Kernel beendet den Container
|
||
(`reason=OOMKilled`).
|
||
|
||
Behoben wird beides, indem die Fassung aus diesem Verzeichnis ausgerollt wird:
|
||
|
||
```bash
|
||
kubectl apply -f k8s/05-app-deployment.yaml
|
||
kubectl -n wabenhain rollout status deploy/wabenhain-app
|
||
```
|
||
|
||
Wird das Deployment über die Rancher-Oberfläche gepflegt, überschreibt das
|
||
nächste Speichern dort ein `kubectl apply` wieder. Dann die Werte in Rancher
|
||
selbst nachziehen (Speicherlimit, beide Probe-Pfade, `timeoutSeconds`) oder das
|
||
Manifest als Quelle übernehmen.
|
||
|
||
Die Anwendungsseite ist unabhängig davon abgesichert: ffmpeg läuft mit
|
||
gedeckelter Threadzahl (`VIDEO_TRANSCODE_THREADS`, Vorgabe 2), damit sein
|
||
Speicher- und CPU-Bedarf nicht mehr von der Größe des Knotens abhängt — siehe
|
||
`src/lib/video-transcode.ts`.
|
||
|
||
**Zusätzlich prüfen:** Hat das Deployment gar kein `uploads`-Volume, ist ein
|
||
erfolgreicher Upload beim nächsten Neustart trotzdem weg. Siehe
|
||
„Bilder/Videos verschwinden nach jedem Redeploy" oben.
|
||
|
||
## Uploads lassen sich nicht speichern
|
||
|
||
Symptom: Im Admin erscheint „Die Datei konnte nicht gespeichert werden. Bitte
|
||
informiere die Technik." Im Pod-Log steht dahinter ein `EACCES`.
|
||
|
||
Ursache ist fast immer der Besitz des gemounteten Volumes. Der Container läuft
|
||
als **`nextjs`, uid 1001, Gruppe `nodejs` gid 1001** (siehe `Dockerfile`). Das
|
||
Image setzt zwar `chown nextjs:nodejs /app/uploads`, aber ein darübergelegtes
|
||
Volume überschreibt das vollständig.
|
||
|
||
Zuerst feststellen, was der Container tatsächlich sieht:
|
||
|
||
```bash
|
||
kubectl -n wabenhain exec deploy/wabenhain-app -- sh -c \
|
||
'id; ls -ld /app/uploads /app/invoices; \
|
||
touch /app/uploads/.probe && echo SCHREIBEN_OK || echo SCHREIBEN_FEHLT'
|
||
```
|
||
|
||
Der Fix hängt am Speicher-Backend — die beiden Wege gelten für verschiedene
|
||
Fälle, nicht alternativ:
|
||
|
||
### Blockspeicher (die meisten CSI-Treiber)
|
||
|
||
`securityContext.fsGroup: 1001` im Deployment. Kubernetes ordnet das Volume
|
||
dann der Gruppe 1001 zu und macht es gruppenschreibbar. Steht bereits in
|
||
`05-app-deployment.yaml`.
|
||
|
||
### SMB/CIFS und NFS
|
||
|
||
**Hier wird `fsGroup` ignoriert.** Diese Protokolle kennen keinen
|
||
per-Datei-Unix-Besitz, den der Knoten nachträglich ändern könnte; ohne
|
||
Zuordnung erscheint alles als `root:root`, und der Container scheitert trotz
|
||
`fsGroup` mit `EACCES`. Die Zuordnung gehört in die `mountOptions` des
|
||
PersistentVolume:
|
||
|
||
```yaml
|
||
spec:
|
||
mountOptions:
|
||
- vers=3.0
|
||
- uid=1001 # muss zum Containerbenutzer passen
|
||
- gid=1001
|
||
- dir_mode=0770
|
||
- file_mode=0660
|
||
- mfsymlinks
|
||
- nobrl # keine Byte-Range-Locks über SMB
|
||
```
|
||
|
||
`dir_mode`/`file_mode` allein genügen nicht: sie bestimmen nur, welche Rechte
|
||
angezeigt werden — wem die Datei gehört, entscheiden `uid`/`gid`.
|
||
|
||
`noperm` (der Client prüft Rechte gar nicht erst) wirkt in vielen Aufbauten
|
||
ebenfalls, ist aber die schwächere Zusicherung: es umgeht die Prüfung, statt
|
||
den Besitz richtigzustellen. `uid`/`gid` also auch dann setzen, wenn `noperm`
|
||
bereits gesetzt ist.
|
||
|
||
**Änderungen an `mountOptions` greifen erst beim nächsten Einhängen.** Nach dem
|
||
Anpassen des PV den Pod neu starten:
|
||
|
||
```bash
|
||
kubectl -n wabenhain rollout restart deploy/wabenhain-app
|
||
```
|
||
|
||
Lässt sich das PV nicht mehr ändern (bei bereits gebundenen PVs je nach
|
||
Kubernetes-Version), PVC und PV löschen und neu anlegen. Bei
|
||
`persistentVolumeReclaimPolicy: Retain` bleiben die Daten auf der Freigabe
|
||
erhalten.
|
||
|
||
## Video-Upload bricht ab (`ECONNRESET`)
|
||
|
||
Symptom im Pod-Log:
|
||
|
||
```
|
||
[upload-video] Verbindung abgebrochen nach 41943040 von angekündigten 78643200 Bytes.
|
||
```
|
||
|
||
Das ist kein Fehler der Verarbeitung — die Datei ist gar nicht erst
|
||
vollständig angekommen. Die Zahlen im Log entscheiden, wo zu suchen ist:
|
||
|
||
- **Immer bei ungefähr derselben BYTEZAHL** → eine Größengrenze auf dem Weg.
|
||
Traefik begrenzt von sich aus nichts; prüfen, ob eine `buffering`-Middleware
|
||
mit `maxRequestBodyBytes` am Ingress hängt.
|
||
- **Immer nach ungefähr derselben ZEIT** → eine Zeitgrenze. Davon liegen
|
||
mehrere hintereinander:
|
||
|
||
| Grenze | Vorgabe | Wo einstellbar |
|
||
|--------|---------|----------------|
|
||
| Node `requestTimeout` | **300 s** | nicht konfigurierbar — harte Obergrenze für die Dauer eines Uploads |
|
||
| Traefik `respondingTimeouts.readTimeout` | je nach Version | `entryPoints.websecure.transport.respondingTimeouts` in der Traefik-Konfiguration |
|
||
| Browser bzw. Netz dazwischen | — | — |
|
||
|
||
Die 300 Sekunden von Node sind die eigentliche Obergrenze: Was in dieser Zeit
|
||
nicht durch die Leitung passt, lässt sich nicht hochladen — unabhängig davon,
|
||
dass `MAX_UPLOAD_VIDEO_BYTES` 100 MB erlaubt. Beispiel: 80 MB über eine
|
||
Upload-Leitung mit 5 Mbit/s brauchen rund 130 Sekunden und gehen durch; bei
|
||
2 Mbit/s sind es über 5 Minuten, und der Upload stirbt zuverlässig.
|
||
|
||
Praktisch heißt das: **das Hero-Video vor dem Hochladen verkleinern.** Der
|
||
Server rechnet es ohnehin auf höchstens 1920 px Breite und CRF 26 um (siehe
|
||
`src/lib/video-transcode.ts`) — was darüber hinausgeht, wird nur übertragen,
|
||
um anschließend weggerechnet zu werden. Ein vorab auf diese Größe gebrachtes
|
||
Video lädt um ein Vielfaches schneller hoch:
|
||
|
||
```bash
|
||
ffmpeg -i original.mp4 -an -vf "scale='min(1920,iw)':-2" \
|
||
-c:v libx264 -preset veryfast -crf 26 -movflags +faststart hero.mp4
|
||
```
|
||
|
||
## 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.
|