Files
Artista-Filmcamp/docs/BETRIEB.md
T
Nick SzaunigandClaude Opus 5.5 c84eb1e740
Docker-Images bauen und veroeffentlichen / build-and-push (push) Successful in 2m17s
refactor(k8s): schlanker Umfang, Postgres und Medien auf SMB
Auf Wunsch des Betreibers auf das Noetige reduziert: StatefulSet
PostgreSQL, Services, Ingress, App-Deployment, zwei SMB-PersistentVolumes
(Postgres-Daten, Medien). Beibehalten nach Absprache: Worker-CronJob
(Folgeraten, Reservierungsfreigabe, Mail-Wiederholung), ConfigMap und die
NetworkPolicies (Sicherheitsbefund Rate-Limit-Umgehung).

- Migration als initContainer im App-Deployment statt eigenem Job
- Entfernt: Backup-CronJob/-Volume (NAS sichert die Freigabe),
  Migrations-CronJob, PodDisruptionBudget, HTTP-Redirect-Ingress und
  Middleware, Namespace-Objekt (einmalig von Hand anzulegen)
- PostgreSQL auf SMB als bewusste, dokumentierte Ausnahme mit
  hard, cache=strict, SMB 3.1.1 + seal, uid 70 / 0700

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-27 20:36:30 +02:00

9.8 KiB

Betrieb auf RKE2

1. Images bauen

Automatisch (Gitea Actions): .gitea/workflows/docker-build.yml baut bei jedem Push auf main, bei Tags v* oder manuell per „Run workflow“ beide Images und pusht sie in die Gitea-Registry:

  • main ergibt die Tags latest und sha-<kurz>
  • v0.1.0 ergibt zusätzlich den Tag 0.1.0

Einmalig einrichten:

  • ein Personal Access Token mit Scope package als Repo-Secret REGISTRY_TOKEN hinterlegen (Repo-Einstellungen → Actions → Secrets)
  • einen Runner mit dem Label docker-builder bereitstellen (wie bei Wabenhain)

Ein Release entsteht dann so: git tag v0.1.0 && git push origin v0.1.0, anschließend den Tag im Overlay setzen.

Manuell:

docker build --target runner -t git.encry.de/nick.rosenloecher/artista-modelcamp:0.1.0 .
docker build --target tools  -t git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0 .
docker push …   # beide Images, immer mit identischem Tag

Der Build braucht Internet für next/font (Schriften werden eingebettet), aber weder Datenbank noch Secrets.

2. Einmalig einrichten

Was ausgerollt wird: StatefulSet PostgreSQL, Services (App, Postgres), Ingress, App-Deployment (mit Migration als initContainer), zwei SMB-Volumes (Postgres-Daten, Medien) und zusätzlich:

  • der Worker-CronJob, nötig für Folgeraten, Reservierungsfreigabe und Mail-Wiederholung
  • ConfigMap
  • zwei NetworkPolicies (Schutz der Rate Limits und der Datenbank)

Eine eigene Sicherung im Cluster gibt es nicht, die SMB-Freigabe wird auf dem NAS gesichert.

  1. Namespace: kubectl create namespace artista und kubectl label namespace artista pod-security.kubernetes.io/enforce=restricted

  2. Overlay anpassen (k8s/overlays/production/): Image-Tag, Domain in kustomization.yaml (APP_URL) und ingress.yaml, ingressClassName (kubectl get ingressclass), ClusterIssuer (Beispiel letsencrypt-prod) und TLS-Secret-Name. Die HTTP→HTTPS-Umleitung erwartet das Manifest in Traefik selbst.

  3. SMB-Speicher (k8s/overlays/production/smb-volumes.yaml):

    1. csi-driver-smb installieren, Befehl steht im Kopf der Datei.
    2. Auf dem NAS die Ordner modelcamp-postgres (leer) und modelcamp-media anlegen und source in der Datei anpassen.
    3. SMB-Secret anlegen: kubectl -n artista create secret generic artista-modelcamp-smb --from-literal=username='…' --from-literal=password='…'. Empfohlen ist ein eigener NAS-Benutzer, der nur diese Ordner sieht.
    4. Das NAS muss SMB 3.1.1 mit Verschlüsselung erlauben (seal), z. B. Synology unter „SMB-Verschlüsselung für Übertragung“, Windows mit Set-SmbShare -EncryptData $true.

    Die Rechte setzen die mountOptions, auf SMB wirkt fsGroup nicht: Postgres uid=70, 0700/0600; Medien uid=1001, für die App nur lesbar (0550/0440).

    PostgreSQL auf SMB ist eine bewusste Ausnahme. SMB garantiert Sperren und fsync nicht in jedem Fall wie ein lokales Dateisystem. Bei einem NAS- oder Netzwerkausfall während Schreibvorgängen kann die Datenbank beschädigt werden. Gemildert wird das durch hard, cache=strict, Verschlüsselung und ein 120 Sekunden langes, sauberes Herunterfahren. Die NAS-Sicherung ist nur mit atomaren Snapshots der Freigabe (Btrfs/ZFS) konsistent. Ein Dateikopie-Backup eines laufenden Datenverzeichnisses lässt sich nicht zuverlässig wiederherstellen. Zusätzlicher Schutz bei Bedarf: gelegentlich einen pg_dump ziehen (Abschnitt 7).

  4. Secret anlegen: Befehl steht in k8s/secret.example.yaml, besser über Sealed Secrets oder den External Secrets Operator.

  5. Anwenden: kubectl apply -k k8s/overlays/production. Der initContainer migriert die Datenbank vor dem App-Start.

  6. Erstes Admin-Konto:

    kubectl -n artista run admin-create --rm -it --restart=Never --labels=artista/db-client=true \
      --image=git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0 \
      --overrides='{"spec":{"containers":[{"name":"admin-create","image":"git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0","command":["tsx","scripts/create-admin.ts","--email","[email protected]","--name","Dein Name"],"envFrom":[{"configMapRef":{"name":"artista-modelcamp-config"}},{"secretRef":{"name":"artista-modelcamp-secrets"}}]}]}}'
    

    Das einmalige Passwort wird ausgegeben. Bei der ersten Anmeldung unter /admin wird die 2FA eingerichtet.

3. Update ausrollen

# Neuen Tag im Overlay setzen und anwenden. Jeder neue App-Pod migriert per
# initContainer "migrate" zuerst die Datenbank; scheitert das, startet er nicht
# und die alten Pods bleiben aktiv (maxUnavailable: 0).
kubectl apply -k k8s/overlays/production
kubectl -n artista rollout status deploy/artista-modelcamp-app
# Migrationsausgabe bei Bedarf:
kubectl -n artista logs deploy/artista-modelcamp-app -c migrate

Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können.

4. Stripe

  • Schlüssel: Im Test sk_test_…, live sk_live_… (nur mit APP_ENV=production). Empfohlen ist ein eingeschränkter Schlüssel mit Schreibrechten auf Customers, Checkout Sessions, PaymentIntents, PaymentMethods und Refunds.
  • Webhook-Endpunkt: https://<domain>/api/stripe/webhook. Diese Events abonnieren: checkout.session.completed, checkout.session.async_payment_succeeded, checkout.session.async_payment_failed, checkout.session.expired, payment_intent.succeeded, payment_intent.payment_failed, charge.refunded, refund.created, refund.updated, charge.dispute.created. Das Signing Secret gehört als STRIPE_WEBHOOK_SECRET ins Secret. Test- und Live-Endpunkt haben getrennte Secrets.
  • Zahlungsarten (Dashboard → Zahlungsmethoden): Für Ratenzahlung nur wiederverwendbare Methoden aktivieren (Karten, gegebenenfalls SEPA-Lastschrift). Checkout blendet bei Ratenzahlung ohnehin nur Methoden ein, die setup_future_usage unterstützen.
  • Test lokal: stripe listen --forward-to localhost:3000/api/stripe/webhook
  • Sicherheitsnetz: Der Worker gleicht bei abgelaufenen Reservierungen und hängenden Einzügen immer mit Stripe ab. Ein verlorener Webhook führt so nicht zu verlorenen Zahlungen.

5. E-Mail

MAIL_TRANSPORT=smtp mit SMTP_HOST, SMTP_PORT, SMTP_SECURE, SMTP_USER und SMTP_PASSWORD. Den Absender (MAIL_FROM) per SPF, DKIM und DMARC für die Absenderdomain freischalten. ADMIN_NOTIFY_EMAIL erhält Hinweise zu „Prüfung nötig“. Nicht zustellbare Mails zeigt das Admin-Dashboard. Der Worker versucht jede Mail bis zu achtmal.

6. Worker

Der CronJob artista-modelcamp-worker läuft alle 10 Minuten und kümmert sich um:

  • abgelaufene Reservierungen, vorher mit Rückfrage bei Stripe
  • hängende Einzüge
  • fällige Raten
  • das Trennen der Zahlungsmethode nach der letzten Rate
  • den E-Mail-Versand
  • das Aufräumen der Rate Limits

Ein fehlgeschlagener Lauf erscheint als fehlgeschlagener Job (kubectl -n artista get jobs).

7. Sicherung und Wiederherstellung

  • Sicherung: Die NAS-Sicherung der SMB-Freigabe übernimmt das Speichersystem. Für PostgreSQL gilt das nur mit atomaren Snapshots, siehe Abschnitt 2. Im Cluster läuft kein Backup-Job.
  • Logischer Dump (empfohlen vor Updates und vor dem Live-Verkauf):
    POD=$(kubectl -n artista get pod -l app.kubernetes.io/name=artista-modelcamp-postgres -o name | head -1)
    kubectl -n artista exec $POD -- pg_dump -U artista -d artista --format=custom > artista-$(date +%F).dump
    
  • Wiederherstellen:
    1. App stoppen: kubectl -n artista scale deploy/artista-modelcamp-app --replicas=0 und den Worker-CronJob pausieren: kubectl -n artista patch cronjob artista-modelcamp-worker -p '{"spec":{"suspend":true}}'
    2. Einspielen: kubectl -n artista exec -i $POD -- pg_restore -U artista -d artista --clean --if-exists < artista-YYYY-MM-DD.dump
    3. App wieder hochskalieren (der initContainer migriert dabei), Worker fortsetzen.
    4. Abgleich: Zahlungen, die nach dem Sicherungszeitpunkt eingingen, im Stripe-Dashboard prüfen. Stripe stellt Webhooks bis zu drei Tage erneut zu (Dashboard → Webhooks → „Erneut senden“).
  • Medien: liegen im SMB-Ordner modelcamp-media und sind in der NAS-Sicherung enthalten.
  • Wiederherstellung üben: mindestens einmal vor dem Live-Verkauf in einen Test-Namespace.

8. Stornierung und Erstattung

Die Software trifft keine Storno-Entscheidung, sie setzt sie nur nachvollziehbar um.

  1. Stornieren (Admin → Buchung → „Stornieren“, Grund ist Pflicht): Offene Raten werden beendet, Platz und Gutschein freigegeben, die gespeicherte Zahlungsmethode getrennt und offene Checkout-Sessions geschlossen. Danach wird nichts mehr abgebucht.
  2. Erstatten (je bezahlter Rate, Betrag frei wählbar, höchstens bis zum gezahlten Betrag): Welcher Betrag erstattet wird, bestimmen die freigegebenen Stornobedingungen. Die Erstattung wird bei Stripe ausgelöst, der Zahlungsstatus aktualisiert sich per Webhook (charge.refunded). Ein doppeltes Abschicken wird über einen Idempotenzschlüssel abgefangen.
  3. Prüfung nötig entsteht bei verspäteter Zahlung ohne freien Platz, Doppelzahlung, Rückbuchung oder endgültig fehlgeschlagener Rate. Dann entweder „Buchung bestätigen“ (nur mit freiem Platz, sonst vorher Kapazität erhöhen) oder stornieren und erstatten.
  4. Dokumentation: Alle Schritte stehen im Protokoll der Buchung (AuditLog) mit handelnder Person.

9. Medien

Dateien (jpg, png, webp, avif, mp4, webm) direkt in den SMB-Ordner modelcamp-media legen, z. B. über den Windows-Explorer (\\nas…\artista\modelcamp-media). Die App sieht sie sofort unter /media/<dateiname>. Im Pod ist das Volume schreibgeschützt eingehängt.

Danach in src/content/site.ts eintragen. Hotlinking fremder Bilder, etwa von Booking.com, ist nicht vorgesehen: Nur Bilder mit geklärter Nutzungsberechtigung ablegen.