# 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-` - `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:** ```bash 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:** ```bash 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","du@example.org","--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 ```bash # 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:///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): ```bash 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/`. 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.