Hosting-Stack Master-Plan
Status: Draft v5 (Pivot zu K3s + Pulumi)
Datum: 2026-05-01
Ziel-Quelle: docs/plans/marketing/roadmap.md Pfad I (I.A → I.B → I.C)
Vorgänger: Coolify-Plan v4 verworfen — mit K8s-Skill im Team ist K3s der direktere Pfad zur SaaS-Phase ohne Re-Migration.
TL;DR
Stack: K3s (lightweight Kubernetes, self-hosted auf Hetzner Cloud) + Pulumi (TS-IaC) + GitOps via FluxCD. Multi-Tenant via Database-pro-Tenant in CloudNativePG-Operator-Cluster, Edge via ingress-nginx + cert-manager + Cloudflare-DNS-Challenge. Backup via Velero zu Hetzner Storage Box.
Warum nicht Coolify (v4-Plan verworfen):
- Re-Migration zu K8s in Phase I.C wäre nochmal 4-6 Wochen — Solo-Dev-Zeit verbrannt
- Multi-Tenant-DB mit live-resize/PITR ist DIY in Coolify, builtin in CloudNativePG
- HPA, traffic-splitting, soft-reload sind K8s-native, in Coolify Custom-Skripte
- Self-Host-Mode für DACH-Mittelstand: Helm-Chart ist bekannter als Coolify-Setup für Enterprise-IT
- Marc kennt Pulumi gut — die ganze Infra wird TS-Code im Repo, nicht UI-State in einer Coolify-DB
API-First: Tenant-Provisioning aus AI-Builder ruft pulumi up mit tenant-config (Pulumi Automation API). Kein UI-Klicken.
Two Distribution Modes (für I.C SaaS):
- kumiko.so SaaS-managed: ein zentraler K3s-Cluster, Database-pro-Tenant, AI-Builder spawned Tenant-Apps via Pulumi-API
- Enterprise Self-Host: Pulumi-Program + Helm-Chart-Bundle, Kunde rennt
pulumi upauf eigenem K8s
Constraints (entschieden)
- EU-only Hosting (DACH-Compliance, DSGVO). Hetzner Cloud (FSN, NBG, HEL). Cloudflare nur DNS+CDN, kein data-at-rest.
- Self-hosted K3s, kein Managed-K8s — Hetzner-Managed (oder EKS/GKE) zu teuer langfristig laut Marc’s Erfahrung.
- Solo-Dev (Marc + AI): Plan ist auf einen ausführenden zugeschnitten. Konkrete Pulumi-Code-Snippets statt vager “use Helm chart”-Anweisungen.
- Pulumi-TS als IaC-Sprache — passt zum Kumiko-Stack, Marc kennt’s am besten. Resources werden im Repo unter
infra/pulumi/versioniert. - API-First: Tenant-Provisioning + Tier-Migration ausschließlich via Pulumi-Automation-API (programmatic) oder kubectl-Apply.
- GitOps für App-Deploys via FluxCD —
git push main= Cluster-State-Update. Kein imperativeskubectl applyvon Hand. - SSH komplett aus auf allen Cluster-Boxes. Admin-Zugang nur via Wireguard-VPN zur Master-Box. Innerhalb Cluster läuft Inter-Node-Traffic über Hetzner-Cloud-Network (10.0.0.0/16, gratis, separates Interface). Kein public-SSH-Angriffsvektor.
Heute-State
┌─────────────────────────────────────┐│ Hetzner CAX21 (publicstatus) ││ ───────────────────────── ││ publicstatus-app (bun, 90 MB) ││ publicstatus-db (postgres) ││ publicstatus-redis ││ publicstatus-caddy (TLS) ││ GHA-Runner (systemd) ││ Deploy: ssh + docker compose pull │└─────────────────────────────────────┘Pain-Points: direct-SSH-touch, Custom-Deploy-Skripte, kein Backup-off-site, kein Monitoring, Showcase-#2/#3-Deploy würde gleichen Pain wiederholen.
Ziel-Topologie
Phase I.A — Foundation (~2-3 Wochen Aufwand)
Box-Naming-Schema:
BOX M1— K3s Master, Wireguard-Server (Admin-VPN), CAX21BOX A1, A2, ... AN— K3s-Agents (Workloads), CAX11 als Default (billigste ARM, 2 vCPU 4 GB ~3.30€/mo)- Existing publicstatus-Box wird BOX A1 nach Migration
┌──────────────────────────────────────────────────┐ │ Hetzner Cloud (NBG/FSN) │ │ │ │ Marc (lokal) ──► Wireguard ──► BOX M1 │ │ K3s Master │ │ CNPG, Velero │ │ ingress-nginx │ │ cert-manager │ │ │ │ │ Hetzner Cloud Network (10.0.0.0/16) │ │ │ │ │ ┌────────┴───────┐ │ │ │ │ │ │ BOX A1 BOX A2│ │ K3s-Agent K3s-Agent│ │ (CAX11) (CAX11) │ │ publicstatus designer │ │ gha-runner kumiko- │ │ landing │ │ │ │ Floating-IP ◄── ingress-nginx (DaemonSet) │ │ (Inbound HTTP/HTTPS) │ └──────────────────────────────────────────────────┘SSH ist auf allen Boxes Internet-aus. Admin-Zugang ausschließlich über Wireguard-VPN. Marc verbindet lokal mit Wireguard-Client zu BOX M1, dann SSH/kubectl über VPN-IP.
Boxen: 1× CAX21 (Master) + 2× CAX11 (Workers) = ~10€/mo + Storage Box 3€/mo + Floating-IP 1€/mo = ~14€/mo.
Migrations-Pfad (nicht zwei parallele Boxen, sondern ein Master neu, dann Existing als A1):
- BOX M1 neu bestellen (CAX21), Wireguard + K3s-Master, SSH-Internet-aus
- K3s-Cluster mit nur Master initial up. Operators installieren via Pulumi.
- publicstatus auf Master deployen (Master darf Workloads laufen —
--node-taintaus). DB-Migration via CNPGreplica-Mode oder pg_dump+restore. - DNS-Cutover Cloudflare auf Floating-IP des Masters.
- Existing publicstatus-Box reset (Hetzner-Rebuild aus snapshot-vorher), als CAX11 oder bleibt CAX21 (Hetzner-Resize von CAX21 → CAX11 möglich, oder Box behalten + CAX11 zusätzlich bestellen für mehr Reserve).
- K3s-Agent auf BOX A1, joined Cluster, Master taintet sich wieder als control-plane-only, Workloads moven zu A1.
BOX A2als zweite Worker für Designer/AI-Builder Phase I.B.
Phase I.B — Designer + AI-Builder (~Woche 6-8)
Cluster wächst: zweite Worker-Node hinzu (3× CAX21 = ~15€/mo). Designer-App + LLM-Gateway als zusätzliche Deployments.
Phase I.C — SaaS-Multi-Tenant (~Phase 4)
┌────────────────────────────────────────────┐ │ K3s Cluster (3-N Workers, je nach Tenants) │ │ │ │ Namespace: tenant-acme │ │ Deployment: app (mem 1Gi, cpu 1) │ │ Deployment: redis │ │ Namespace: tenant-foo │ │ Deployment: app (mem 4Gi, cpu 2) │ │ ... │ │ │ │ Namespace: cnpg-system │ │ Cluster: shared-postgres │ │ Database: tenant_acme │ │ Database: tenant_foo │ │ Backup: PITR via WAL-archive → S3 │ └────────────────────────────────────────────┘CloudNativePG: ein Postgres-Cluster managed alle Tenant-DBs, mit live-resize, point-in-time-recovery, automatischem WAL-archive zu S3-Storage-Box.
Stack-Components (alle K8s-native)
| Component | Wahl | Rolle |
|---|---|---|
| Cluster | K3s | Lightweight K8s, Single-Binary, ARM-fähig, Cloud-Native |
| IaC | Pulumi-TS | Cluster-Setup, Apps, Tenants — alles Code im Repo |
| GitOps | FluxCD | git push main → Cluster-State, kein imperative kubectl |
| Ingress | ingress-nginx | Industry-standard, soft-reload-fähig, weighted-routing |
| Cert-Mgmt | cert-manager | Let’s-Encrypt mit DNS-01-Challenge (Cloudflare) für Wildcards |
| Postgres | CloudNativePG | Operator für PG-Cluster + Database-CRD, PITR builtin |
| Redis | spotahome/redis-operator | falls needed (Sentinel/Cluster), Standard-StatefulSet sonst |
| Backup | Velero | Cluster-resources + PVCs zu S3 (Storage Box) |
| Monitoring | kube-prometheus-stack | Prometheus + Grafana + Alertmanager (Helm-Chart) |
| Logs | Loki + Promtail | log-aggregation in Grafana, kein eigener Stack |
| Secrets | sealed-secrets oder Pulumi-ESC | Pulumi-Secrets bleiben encrypted im Repo |
| DNS | Cloudflare-API | externer-DNS-Operator pflegt Records automatisch |
Pulumi-Architektur (3 Stacks)
infra/pulumi/├── platform/ # Cluster + System-Services (kumiko.so SaaS)│ ├── index.ts # K3s nodes, internal network, SSH-keys│ ├── operators.ts # CNPG, ingress-nginx, cert-manager, velero│ └── monitoring.ts├── apps/ # Showcases + Designer + AI-Builder│ ├── publicstatus.ts│ ├── kumiko-landing.ts│ ├── designer.ts│ └── gha-runner.ts└── tenant/ # Tenant-Provisioning (Phase I.C) ├── stack.ts # Generisch parametrisierbar pro Tenant └── tier.ts # Resource-Limits per Tierplatform-Stack ist einmalig deployed, langlebig.
apps-Stack wird per FluxCD via Git-Push deployed (Pulumi rendert nur Manifests, FluxCD applied).
tenant-Stack wird programmatic per Tenant via Pulumi-Automation-API gespawned aus AI-Builder.
Tenant-Spawn pseudo-code (aus AI-Builder):
import * as pulumi from "@pulumi/pulumi/automation";
async function provisionTenant(tenantId: string, tier: "free" | "starter" | "pro") { const stack = await pulumi.LocalWorkspace.createOrSelectStack({ stackName: `tenant-${tenantId}`, workDir: "infra/pulumi/tenant", }); await stack.setConfig("tenantId", { value: tenantId }); await stack.setConfig("tier", { value: tier }); const result = await stack.up({ onOutput: console.log }); return { url: result.outputs["url"].value };}Tier-Migration: stack.setConfig(“tier”, “pro”) + stack.up() — Pulumi diff’d resources, aktualisiert Resource-Limits, K8s rolling-restart. ~30s Downtime.
Differentiator — Seamless Scaling
(Dieser Abschnitt war Marketing-USP-Gold im v4-Plan, bleibt unverändert wichtig — die Implementation ist nur K8s-native statt Coolify-Custom.)
| Was wir versprechen | Wie K8s das löst |
|---|---|
| Tier-Upgrade in <30s, kein Datenverlust | kubectl set resources rolling restart, App-Container stateless |
| Box-Move mit <2 min Downtime | Velero-Backup + Restore, oder PVC-CSI-Migration zwischen Nodes |
| DB-Migration ohne App-Stop | CloudNativePG live-resize, schema-changes via online-additive (C7) |
| Backup ist immer aktuell + restorebar | CNPG WAL-archive (kontinuierlich) + Velero daily, quarterly Restore-Drill |
| DSGVO-Export <5 min | Pulumi-Job: kubectl exec pg-cluster -- pg_dump tenant_X → S3 → presigned-link |
| HAProxy-Switch ohne Connection-Drop | ingress-nginx soft-reload (built-in, lua-balancer) |
| Phased Tier-Migration (10% → 100%) | ingress-nginx canary-annotations: weight, header, cookie |
| Auto-Scale on Load | HorizontalPodAutoscaler — Tenant-App skaliert mit CPU/RAM-Metric |
Marketing-Use: Demo-Video Phase 3 — “1-Click Tier-Upgrade mit Live-Traffic-Switch via HPA + canary-rollout” als hochwertigster Hook für DACH-Pitch. Cross-Reference: docs/plans/marketing/roadmap.md S8.
Tenant-App Operations
Tenant-Topologie (Database-pro-Tenant in shared CNPG-Cluster)
Pro Tenant:
apiVersion: v1kind: Namespacemetadata: { name: tenant-acme }---apiVersion: postgresql.cnpg.io/v1kind: Databasemetadata: namespace: cnpg-system name: tenant-acmespec: cluster: { name: shared-postgres } name: tenant_acme owner: tenant_acme---apiVersion: apps/v1kind: Deploymentmetadata: namespace: tenant-acme name: appspec: template: spec: containers: - name: app image: ghcr.io/cosmicdriftgamestudio/tenant-runtime:latest resources: limits: { memory: 1Gi, cpu: "1" } requests: { memory: 512Mi, cpu: 250m } env: - name: DATABASE_URL value: postgres://tenant_acme:$(PASSWORD)@shared-postgres-rw.cnpg-system/tenant_acmeCNPG managed shared-postgres als Cluster, jede Database-CRD spawn’d eine eigene DB im selben Cluster — Backup-pro-Database-trivial via pg_dump-Job, Migration zwischen Clustern via pg_basebackup+restore.
Resource-Tiers (initial)
| Tier | RAM | CPU | Disk-PVC | DB-Größe |
|---|---|---|---|---|
| Free | 256Mi | 0.25 cpu | 1 GB | 500 MB |
| Starter | 1 Gi | 1 cpu | 10 GB | 5 GB |
| Pro | 4 Gi | 2 cpu | 50 GB | 25 GB |
| Enterprise | custom | custom | custom | custom |
Pulumi-tier-config:
const TIER_LIMITS = { free: { memory: "256Mi", cpu: "0.25", storage: "1Gi" }, starter: { memory: "1Gi", cpu: "1", storage: "10Gi" }, pro: { memory: "4Gi", cpu: "2", storage: "50Gi" },};Tier-Migration
Standard-Upgrade (Free → Starter):
await stack.setConfig("tier", { value: "starter" });await stack.up(); // Pulumi diff: resource-limits changed → K8s rolling restartDowntime: ~10s (rolling deployment, 1 replica).
Cross-Node-Move (Starter → Pro mit dedicated Node):
- Tier-config setzt
nodeAffinityauf “pool=pro-pool” - K8s scheduler moves pod
- DB bleibt im shared-cluster (kein Move)
- Total Downtime: ~30s
Self-managed-Postgres-Move (Enterprise mit dediziertem PG-Cluster):
- CNPG kann live-replica setup zu neuem Cluster, dann promoted-cutover
- Downtime: ~5s (DNS-cutover)
Backup-Strategy (3 Layer)
Layer 1 — CNPG WAL-Archive (kontinuierlich):
spec: backup: barmanObjectStore: destinationPath: s3://kumiko-backups/cnpg/ s3Credentials: { ... } retentionPolicy: "30d"Point-in-Time-Recovery auf jede Sekunde der letzten 30 Tage.
Layer 2 — Velero Daily Cluster-Snapshot:
velero schedule create daily \ --schedule="0 3 * * *" \ --snapshot-volumes \ --include-namespaces tenant-*,cnpg-systemVolumes + K8s-Resources zu S3-Storage-Box, Retention 7 daily / 4 weekly / 3 monthly.
Layer 3 — Quarterly Restore-Drill:
Pulumi-Stack restore-drill spawn’d ad-hoc Test-Cluster, restored Backup, smoke-test, teardown. Audit-Log dokumentiert success.
Admin-Access — Wireguard
Alle Cluster-Boxes haben SSH-Internet-aus (Hetzner-Cloud-Firewall blockiert Port 22 von 0.0.0.0/0). Admin-Zugang ausschließlich über Wireguard-VPN.
Setup
BOX M1 (Wireguard-Server):
# Pulumi-Provisioner oder cloud-init:apt install -y wireguardwg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
cat > /etc/wireguard/wg0.conf <<'EOF'[Interface]Address = 10.10.0.1/24ListenPort = 51820PrivateKey = <server-private-key>
# Marc lokal[Peer]PublicKey = <marc-laptop-public-key>AllowedIPs = 10.10.0.10/32EOF
systemctl enable --now wg-quick@wg0Hetzner-Cloud-Firewall für BOX M1:
- Inbound TCP 51820 → 0.0.0.0/0 (Wireguard handshake, encrypted)
- Inbound TCP 22 → KEIN
- Inbound TCP 6443 (K8s API) → 10.10.0.0/24 only (über Wireguard)
Marc lokal:
[Interface]Address = 10.10.0.10/24PrivateKey = <marc-private>
[Peer]PublicKey = <server-public>Endpoint = <m1-public-ip>:51820AllowedIPs = 10.10.0.0/24, 10.0.0.0/16 # auch Cluster-internal-NetworkPersistentKeepalive = 25wg-quick up kumiko # VPN-tunnel aktivssh root@10.10.0.1 # SSH zum Master via VPNkubectl get nodes # K8s-API erreichbar via VPNWorker-Nodes (BOX A1, A2):
- KEIN eigener Wireguard-Server (saved RAM)
- SSH via Master-Box als Jump-Host:
ssh -J root@10.10.0.1 root@10.0.1.20 - ODER Worker im selben Wireguard-Peer-Setup, dann direkt erreichbar — ist 5min extra config, falls Komfort wichtig
- K3s-Inter-Node-Traffic geht über Hetzner-Cloud-Network (10.0.0.0/16), nicht durch Wireguard
Why Wireguard statt SSH-Bastion
- Wireguard ist Kernel-Modul, ~15 MB RAM, schneller als SSH-tunnel
- Single Endpoint pro Operator (ein WG-Tunnel öffnet alle Cluster-Resources)
- Kein SSH-public-port nötig — Internet-SSH-Brute-Force-Versuche werden 0
- kubectl/Helm/Pulumi laufen direkt ohne SSH-Forwarding-Magic
Edge-Layer (ingress-nginx)
Internet ─► Cloudflare DNS ─► [Hetzner Floating-IP] │ ingress-nginx (DaemonSet auf 2-3 Workers) │ ├─► Service: tenant-acme/app ├─► Service: tenant-foo/app └─► …Cert-Manager mit Cloudflare-DNS-01:
- Wildcard-Cert für
*.kumiko.sound*.tenant.kumiko.so - Auto-Renewal alle 60 Tage
- Cloudflare-API-Token in K8s-Secret (Pulumi-managed)
Floating-IP: Hetzner Cloud Floating-IP (1€/mo) wird zwischen 2-3 Worker-Nodes gerouted — Failover bei Node-Ausfall ohne DNS-Update.
Single A-Record auf Cloudflare: *.kumiko.so → Floating-IP. Keine Cloudflare-IP-Pflege wenn Worker-Nodes wechseln.
Self-Host-Mode (Enterprise)
Customer hat eigenes K3s/K8s. Wir liefern:
kumiko-enterprise-bundle/├── pulumi/│ ├── Pulumi.yaml│ ├── index.ts # Single-tenant-deployment für eigenes Cluster│ └── README.md├── helm/│ ├── kumiko-app/ # alternative wenn Kunde kein Pulumi nutzt│ └── kumiko-postgres/└── docker-compose/ # für Kunden ohne K8s (degraded-mode, kein scale) └── docker-compose.ymlCustomer rennt pulumi up auf eigenem K8s — Tenant-Stack kommt hoch, ist single-tenant (keine Multi-Tenant-Logik nötig). Update: git pull && pulumi up.
DACH-Mittelstand-Pitch: “Eure DevOps kennt Helm/Pulumi schon. Wir liefern das Bundle, ihr habt volle Kontrolle, wir haben null-Zugriff auf eure Daten.”
Migration-Roadmap
Schritt 0 — entfällt (publicstatus ist Demo, fresh-deploy)
Da nichts zu migrieren ist, brauchen wir keine detaillierte Stack-Doku. Source-of-Truth ist samples/showcases/publicstatus/deploy/docker-compose.yml im Repo — daraus rendern wir K8s-Manifests in Schritt 3.
Einzige Vor-Arbeit: DNS-Records (welche Domains zeigen heute auf publicstatus-Box) festhalten — das sind publicstatus.eu und ggf. Subdomains. Ein 30-Sekunden-Check.
Schritt 1 — BOX M1 bestellen + Wireguard + K3s-Master (~1-2d)
- Hetzner Cloud Network
kumiko-internal(10.0.0.0/16) erstellen - BOX M1 (CAX21) mit Pulumi/Hetzner-API bestellen, ins Network attachen, IP
10.0.1.10 - Cloud-Init:
- Wireguard-Server-Config (siehe Admin-Access-Section)
curl -sfL https://get.k3s.io | sh -s - server --node-ip 10.0.1.10- Hetzner-Cloud-Firewall: nur Wireguard-Port 51820 + HTTP/HTTPS public, SSH-Internet aus
- Marc-lokal: Wireguard-Client setup,
kubectl get nodesüber VPN funktioniert
Schritt 2 — System-Operators auf Master (~3-5d)
Über Pulumi-K8s-Provider auf Master-only-Cluster (Master darf temporär Workloads laufen):
- ingress-nginx (Helm) — DaemonSet, hört auf Floating-IP
- cert-manager + Cloudflare-Issuer
- CloudNativePG-Operator + erstes
Cluster-Resource (shared-postgres) - Velero mit Hetzner-Storage-Box-S3-Endpoint
- FluxCD-Bootstrap auf
main-branch (infra/k8s/) - kube-prometheus-stack + Loki/Promtail
Schritt 3 — publicstatus auf K3s deployen (~1d)
publicstatus ist nur Demo — keine echten Kundendaten, kein DB-Migration-Bedarf. Fresh-Deploy genügt.
- CNPG
Database-Resource für publicstatus - publicstatus-Deployment mit
DATABASE_URL=postgres://...@shared-postgres-rw.cnpg-system/publicstatus - ENV-Secret für JWT_SECRET, DB_PASSWORD, DEMO_ADMIN_* (Pulumi-managed via
pulumi config set --secret) - Demo-Seed läuft automatisch beim Container-Start (existing publicstatus migration-hooks)
- Test via temp-domain
publicstatus-staging.kumiko.so(Cloudflare-Record auf neue Floating-IP) - Smoke-Test: HTTP 200 auf
/health, demo-admin-login, eine Component sichtbar - Cloudflare-DNS für
publicstatus.euauf Floating-IP. Down-Time: 0-60s je nach TTL. - Demo-Daten der alten Box sind irrelevant.
Schritt 4 — Old publicstatus-Box reset → BOX A1 (~1d)
- 24h-Beobachtung nach Cutover. Wenn alles ruhig:
- Old-Box: alte Apps stoppen, Hetzner-Rebuild auf saubere Ubuntu-24-Image
- Optional: Resize CAX21 → CAX11 (billiger), oder behalten für mehr Reserve
- Pulumi-Resource: BOX A1 mit
agent-userData:curl -sfL https://get.k3s.io | K3S_URL=https://10.0.1.10:6443 K3S_TOKEN=<...> sh - - BOX A1 joined Cluster automatisch
- Master-Box: taintet sich
node-role.kubernetes.io/control-plane:NoSchedule→ Workloads moven zu A1 - GHA-Runner als StatefulSet mit RUNNER_TOKEN-Secret (RUNNER_LABELS=self-hosted,kumiko,arm64)
Schritt 5 — Monitoring + Backup operativ (~2d)
- Velero erste Backup-Run + Restore-Drill auf temp-Pulumi-Stack
- Alertmanager-Rules: pod-restart-loops, disk-full, cert-expiry-30d, Postgres-replica-lag
- Status-Page (kumiko’s PublicStatus-Instance) für Cluster-Health: scrape Prometheus, render in einem zweiten publicstatus-Deployment
Schritt 6 — Phase I.B Trigger: Designer + LLM-Gateway
Wenn C5/C6 ready: BOX A2 (CAX11 oder CAX21 je nach LLM-Gateway-RAM-Bedarf), Designer-App + LLM-Gateway als Deployments im apps-Stack.
Schritt 7 — Phase I.C Trigger: Multi-Tenant-Provisioning
Eigener Plan-Doc nötig (docs/plans/architecture/saas-multitenancy.md). Pulumi-Automation-API von AI-Builder, Tenant-spawn-flow, billing-integration, custom-domains.
Aufgaben-Aufteilung (Solo-Dev = Marc + AI)
Was AI machen kann:
- Pulumi-TS-Code (Resources, Cloud-Init, Helm-Values)
- Cloud-Init-Scripts (Wireguard, K3s-install, firewall-rules)
- K8s-Manifests, Operator-Configs, Pulumi-Stack-Code
- Pulumi-State-Reads (
pulumi stack output) - SSH zu existing publicstatus (mit existing credentials)
- Plan-Doku, README, Migration-Skripte
- Git-Operations
Was Marc selbst machen muss (Auth-bound):
- Hetzner-API-Token generieren
- Cloudflare-API-Token generieren
- Pulumi-Account / Backend-Choice
- Wireguard-Client lokal: Schlüsselpaar generieren, kumiko.conf editieren,
wg-quick up pulumi uprennen (state-changing, sollte unter Marc’s Auth laufen)- DNS-Cutover-Klick bei Cloudflare (Pulumi macht das auch via API, aber finaler Switch sollte bewusst sein)
- FluxCD GH-token erstellen + bootstrap
- Hetzner-UI-Browse für initial-setup-checks (Account, Project, kein Auto-Bestellen)
Tokens & Secrets-Flow:
- Marc generiert Tokens in jeweiligem UI
- Speichert via
pulumi config set --secret hetznerToken <token>— landet encrypted in Stack-State - AI sieht Token-Werte NIE. Pulumi-Code referenziert via
pulumi.Config.requireSecret("hetznerToken") - Lokale
.env-Files: in.gitignore, nie committed
Open Questions
- K3s-Cluster-Backup — Velero macht PVC + Resources. Etcd-Backup separat? K3s hat eigenen sqlite-state (kein etcd default).
k3s etcd-snapshotals CronJob. - Single-Master-K3s vs HA — Phase I.A: 1 Master OK (Apps laufen weiter wenn Master kurz down ist). Phase I.B/I.C: 3 Master (HA via embedded etcd) oder externe DB für K3s-state.
- Pulumi-Backend — Pulumi-Cloud (free für solo) vs self-hosted Pulumi-Backend (S3 als state). Solo: Pulumi-Cloud free reicht.
- GHA-Runner-ARC vs Standalone — actions-runner-controller (offiziell) ist HPA-fähig (Auto-scale Runner mit CI-Load) — overkill für solo, Standalone-Runner als StatefulSet reicht.
- Tenant-DB-Connection-Pooling — pgbouncer-CRD von CNPG für Tenant-DBs? Bei <50 Tenants nicht nötig.
- Image-Registry —
ghcr.ioreicht für Phase I.A. Bei Self-Host-Enterprise muss Customer eigene Registry haben oder wir bieten Pull-Token. Einfacher: customer mirror viaskopeo copy.
Was bewusst NICHT
- Managed K8s (Hetzner Cloud Native, EKS, GKE) — zu teuer langfristig (Marc’s Erfahrung).
- Service-Mesh (Istio, Linkerd) — Overhead für unsere Cluster-Größe, gain-zero bis 100+ Microservices.
- Multi-Cluster-Federation — DR ist Velero-restore + DNS-Switch, einfacher als Federation.
- eBPF-CNI (Cilium statt flannel/calico) — gain-zero für unseren Use-Case.
- Tenant-isolation via Sandbox-VMs (Kata-Containers, gVisor) — Phase I.C-Frage, nicht heute. Default ist Pod-isolation (gut genug für Mittelstand).
Verworfen: Coolify (Plan v4)
Coolify-Plan war strukturell solide aber:
- Re-Migration zu K8s in I.C wäre verbrannte Solo-Dev-Zeit — 4-6 Wochen Re-Setup, während K3s-from-Start nur ~2-3 Wochen mehr ist initial.
- Multi-Tenant-Operations sind in Coolify Custom-Skripte — gleiches in K8s ist 1 CRD oder 1 HPA-Manifest.
- Pulumi vs Coolify-API — Pulumi ist mächtiger, declarative, version-controlled. Coolify-API ist OK aber UI-State-driven.
- Self-Host-Mode — Helm-Chart für Kunden ist erwartbarer Standard als Coolify-Setup.
Coolify-Plan-v4 ist in git-history (commit 5b7e4568 und Vorgänger) für reference.
Roadmap-Bezug
Dieser Plan-Doc deckt Pfad I.A vollständig + Setup für I.B/I.C ab. Mapping zu docs/plans/marketing/roadmap.md:
| Marketing-ID | Hosting-Plan |
|---|---|
| I2 (Stack-Doku) | Schritt 0 |
| I3 (publicstatus harden + auto-deploy) | Schritte 1-3 |
| I4 (Showcase-Deploy-Pipeline) | FluxCD via Schritt 2 |
| I5 (Telemetry) | Schritt 4 |
| I6 (Backup) | Schritt 4 |
| I7 (SSL/Wildcard) | Schritt 2 (cert-manager) |
| I8 (Kumiko-Status-Page) | Schritt 3 (zweite publicstatus-Instanz) |
| I9 (Designer-Hosting) | Schritt 5 |
| I10-I11 (LLM-Gateway, BYOK) | Schritt 5 |
| I12 (AI-Builder Hosting) | Schritt 5 (zweite App) |
| I15-I21 (SaaS-Tenant-Provisioning) | Schritt 6 — eigener Plan-Doc |