Zum Inhalt springen

Deploy: K3s (Production-Scale)

Pulumi-managed K3s-Deploy — ein Cluster, der jede Kumiko-gebaute App + die Plattform fährt. Läuft so in Production auf kumiko.so.

Ebenen

Der Cluster besteht aus drei aufeinander aufbauenden Pulumi-Stacks:

StackLiegt inWas er liefert
platforminfra/pulumi/platform/Hetzner-VMs, K3s-Install, Wireguard-VPN
operatorsinfra/pulumi/operators/ingress-nginx, cert-manager, CloudNativePG, Velero
sitesinfra/pulumi/sites/Pro-App Deployment + Service + Ingress + Certificate

platform einmal hochziehen, operators einmal, danach pro App einen Eintrag in sites ergänzen.

Neue Site hinzufügen

infra/pulumi/sites/index.ts nutzt einen createStaticSite-Helper, der das Deployment + Service + Ingress + Certificate-Combo bündelt:

createStaticSite({
name: "my-app",
domain: "my-app.kumiko.so",
image: "ghcr.io/your-org/my-app:latest",
// ghcrPull nur falls das Image privat ist
ghcrPull: { username: cfg.requireSecret("ghcrUser"), password: cfg.requireSecret("ghcrToken") },
k8sProvider,
});

Danach pulumi up aus infra/pulumi/sites/ ausführen.

Der Helper setzt:

  • imagePullPolicy: Always — zieht bei jedem Pod-Restart frisches :latest
  • cert-manager.io/cluster-issuer: letsencrypt-prod-Annotation am Ingress
  • nginx-Ingress-Class
  • DNS-Eintrag für die Domain (sofern der Cloudflare-Provider konfiguriert ist)

Rolling-Update bei neuem Image-Push

:latest-Tag + Always-Pull ist nur die halbe Miete. K8s startet Pods nicht neu, wenn der Image-Tag-String unverändert bleibt. Zwei Optionen:

  1. Annotation-Bump — einen unique Wert (z. B. SHA) in eine Pod-Template-Annotation des Deployments schreiben. K8s sieht den Spec-Wechsel → Rolling-Restart.
  2. kubectl rollout restart deployment/my-app — expliziter Restart aus CI nach dem Image-Push.

Beides funktioniert. Option 1 hält Pulumi als Source-of-Truth; Option 2 ist ein zusätzlicher Schritt im Build-Workflow.

Migrate vor Pod-Start

K3s-Deployments nutzen Init-Container für den Pre-Deploy-Migrate-Step:

spec:
template:
spec:
initContainers:
- name: migrate
image: ghcr.io/your-org/my-app:latest
command: ["bun", "/app/kumiko.js", "migrate", "apply"]
env:
- name: DATABASE_URL
valueFrom: { secretKeyRef: { name: my-app-db, key: url } }

Der Pod startet gar nicht erst, wenn der Migrate fehlschlägt. Das Boot-Gate innerhalb der App ist das zweite Netz (SchemaDriftError, wenn das Schema trotzdem vom Journal abweicht).

Backups

Der operators-Stack bringt Velero (K8s-Resources + PVC-Snapshots) und CloudNativePGs barman-cloud (kontinuierliches WAL-Archivieren) mit. Beides sichert nach Hetzner Object Storage (S3-kompatibel).

Daily K8s-Backup um 03:00 UTC, 7-Tage-Retention. Postgres-Backups sind kontinuierlich mit 30-Tage-Retention.

Multi-Tenant auf einem einzigen Postgres-Cluster

CloudNativePG erzeugt einen Postgres-Cluster (pg). Jede App bekommt darin ihre eigene Datenbank — Tenant-Provisioning legt Datenbanken on-demand an. Siehe Architecture: Tenant-DB-Context für das Per-Tenant-Connection-Routing.