docs: README, k8s/README und EINRICHTUNG auf Stand main (2026-10-16)
Docker Image bauen und veroeffentlichen / lint (push) Successful in 1m41s
Docker Image bauen und veroeffentlichen / typecheck (push) Successful in 1m42s
Docker Image bauen und veroeffentlichen / test (push) Successful in 2m40s
Docker Image bauen und veroeffentlichen / build-and-push (push) Successful in 8m21s

README: Abschnitt Projektstand (Issues #34, #33), veraltete Aussagen zu
Platzhaltern und Rechnungsnummern korrigiert, CI-Gate, Rollout per set
image, Client-IP hinter Cloudflare, VAT_RATE_PERCENT_SONSTIGES, Teststand.
k8s/README: Image-Pinning, Abweichungen Repo/Cluster, Sicherung auf der
Storage Box, SMB 3.1.1 mit seal. EINRICHTUNG: APICORE_WEBHOOK_SECRET,
ERP_EXPORT_TOKEN, Abschnitt Neuen Build ausrollen. .env.example:
DATABASE_POOL_SIZE.

Refs #32

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 08:01:07 +02:00
1 parent 24df111495
commit 96c101804d
4 files changed
+112 -40

No files matched your search

+58 -24
View File
@@ -9,9 +9,12 @@ 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.
1. **Gitea Actions**: `.gitea/workflows/docker-build.yml` prueft bei jedem
Push auf `main` lint, audit, typecheck und test und baut erst danach das
Image `<registry>/<owner>/wabenhain:latest` (+ `:sha-<kurzhash>`,
`:vX.Y.Z` bei Tags) in der Gitea-Container-Registry. Daneben entsteht
mit denselben Tags `wabenhain-tools` fuer die CronJobs in
`07-backup-cronjob.yaml`.
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
@@ -53,8 +56,9 @@ alles laeuft: [../docs/EINRICHTUNG.md](../docs/EINRICHTUNG.md).
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
`git.encry.de/nick.rosenloecher/wabenhain` und ist auf einen festen Build
gepinnt (`:sha-<commit>@sha256:<digest>`), nie auf `:latest` — siehe
„Bei einem neuen Image“ unten. 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
@@ -91,10 +95,25 @@ der erste Pod bereit ist.
## Bei einem neuen Image (Redeploy)
Das Deployment ist auf Tag **und** Digest gepinnt. Ein `rollout restart`
holt deshalb kein neues Image, sondern startet nur denselben Build neu (das
genuegt nach einer Aenderung an Secret oder ConfigMap). Einen neuen Build
rollt man so aus:
```bash
kubectl rollout restart deployment/wabenhain-app -n wabenhain
# Digest des gewuenschten Builds aus der Gitea-Registry (Paket "wabenhain",
# Tag sha-<kurzer Commit>) uebernehmen:
kubectl -n wabenhain set image deploy/wabenhain-app \
app=git.encry.de/nick.rosenloecher/wabenhain:sha-<commit>@sha256:<digest>
kubectl -n wabenhain rollout status deploy/wabenhain-app
```
Danach dieselbe Zeile in `05-app-deployment.yaml` nachziehen und
committen, damit Repo und Cluster nicht auseinanderlaufen. Laufen die
CronJobs aus `07-backup-cronjob.yaml`, gehoert ihr `wabenhain-tools`-Image
auf denselben Build (sie nutzen denselben Prisma-Client und muessen zum
Datenbankschema passen).
Migrationen + Seed laufen automatisch erneut beim Start jedes neuen Pods
(idempotent, siehe oben).
@@ -114,13 +133,24 @@ Redeploy.
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.
- Kein `HorizontalPodAutoscaler`, kein `PodDisruptionBudget`. Die
`NetworkPolicy`s (`default-deny-ingress`, `allow-same-namespace`,
`allow-from-traefik`) und die Traefik-Middleware `cloudflare-only` liegen
nicht hier, sondern in den Cluster-Manifesten (Haertung vom 2026-10-12,
Ordner `rke2-cluster/`, nicht unter Git).
- Repo und Cluster weichen voneinander ab (Stand 2026-10-16): Im Cluster
sind `wabenhain-uploads` (5Gi) und `wabenhain-invoices` (50Gi)
`ReadWriteMany` auf SMB mit eigenen PersistentVolumes, hier stehen
`ReadWriteOnce`-Claims gegen die Default-StorageClass (5Gi/2Gi).
`STRIPE_PUBLISHABLE_KEY` liegt im Cluster im Secret, hier in der
ConfigMap (beides funktioniert, weil die App beide Quellen per `envFrom`
liest). Vor einem `kubectl apply` auf ein bestehendes Cluster die
Unterschiede pruefen.
- 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
liegt aber auf derselben Storage Box wie die Datenbank 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
@@ -263,7 +293,8 @@ PersistentVolume:
```yaml
spec:
mountOptions:
- vers=3.0
- vers=3.1.1
- seal # SMB-Verschluesselung, die Storage Box liegt im Internet
- uid=1001 # muss zum Containerbenutzer passen
- gid=1001
- dir_mode=0770
@@ -334,9 +365,14 @@ ffmpeg -i original.mp4 -an -vf "scale='min(1920,iw)':-2" \
## 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,
`07-backup-cronjob.yaml` legt jede Nacht um 03:15 Uhr (Zeitzone des
Clusters, meist UTC) eine gzip-komprimierte `pg_dump`-Sicherung und ein
`rechnungen-<Zeitstempel>.tar.gz` der Rechnungsablage auf einem eigenen
Volume ab und haelt beides 30 Tage vor. Das Volume ist ein SMB-PV auf der
Storage Box (`//u652065.your-storagebox.de/backup/Server/Disks/Customer/Wabenhain`,
Unterordner `db-backup`). Im Cluster ist der Job seit 2026-10-10
angelegt; die letzten drei Laeufe waren erfolgreich, zuletzt am 2026-10-16
um 03:15 UTC. 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).
@@ -354,16 +390,14 @@ kubectl logs -n wabenhain -l job-name --tail=50 # Ausgabe des letzten Laufs
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
```
**Aus dem Cluster herausziehen.** Die Sicherung schuetzt gegen
Volume-Verlust und Bedienfehler, nicht gegen den Ausfall der Storage Box
oder des Standorts. Deshalb regelmaessig (oder per eigenem Cron auf einem
Rechner ausserhalb) an einen zweiten Ort kopieren. Die Dateien liegen auf
der Storage Box unter `Server/Disks/Customer/Wabenhain/db-backup/` und
lassen sich dort direkt abholen (SMB, SFTP oder rsync der Storage Box). Ein
`kubectl exec` in den Sicherungs-Pod geht nicht: der Pod ist nach dem Lauf
beendet (`Completed`).
**Wiederherstellen.** Vorher die App anhalten, damit waehrend des
Einspielens niemand schreibt — `--clean --if-exists` im Dump loescht die