Skip to content

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):

  1. kumiko.so SaaS-managed: ein zentraler K3s-Cluster, Database-pro-Tenant, AI-Builder spawned Tenant-Apps via Pulumi-API
  2. Enterprise Self-Host: Pulumi-Program + Helm-Chart-Bundle, Kunde rennt pulumi up auf 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 imperatives kubectl apply von 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), CAX21
  • BOX 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):

  1. BOX M1 neu bestellen (CAX21), Wireguard + K3s-Master, SSH-Internet-aus
  2. K3s-Cluster mit nur Master initial up. Operators installieren via Pulumi.
  3. publicstatus auf Master deployen (Master darf Workloads laufen — --node-taint aus). DB-Migration via CNPG replica-Mode oder pg_dump+restore.
  4. DNS-Cutover Cloudflare auf Floating-IP des Masters.
  5. 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).
  6. K3s-Agent auf BOX A1, joined Cluster, Master taintet sich wieder als control-plane-only, Workloads moven zu A1.
  7. BOX A2 als 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)

ComponentWahlRolle
ClusterK3sLightweight K8s, Single-Binary, ARM-fähig, Cloud-Native
IaCPulumi-TSCluster-Setup, Apps, Tenants — alles Code im Repo
GitOpsFluxCDgit push main → Cluster-State, kein imperative kubectl
Ingressingress-nginxIndustry-standard, soft-reload-fähig, weighted-routing
Cert-Mgmtcert-managerLet’s-Encrypt mit DNS-01-Challenge (Cloudflare) für Wildcards
PostgresCloudNativePGOperator für PG-Cluster + Database-CRD, PITR builtin
Redisspotahome/redis-operatorfalls needed (Sentinel/Cluster), Standard-StatefulSet sonst
BackupVeleroCluster-resources + PVCs zu S3 (Storage Box)
Monitoringkube-prometheus-stackPrometheus + Grafana + Alertmanager (Helm-Chart)
LogsLoki + Promtaillog-aggregation in Grafana, kein eigener Stack
Secretssealed-secrets oder Pulumi-ESCPulumi-Secrets bleiben encrypted im Repo
DNSCloudflare-APIexterner-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 Tier

platform-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 versprechenWie K8s das löst
Tier-Upgrade in <30s, kein Datenverlustkubectl set resources rolling restart, App-Container stateless
Box-Move mit <2 min DowntimeVelero-Backup + Restore, oder PVC-CSI-Migration zwischen Nodes
DB-Migration ohne App-StopCloudNativePG live-resize, schema-changes via online-additive (C7)
Backup ist immer aktuell + restorebarCNPG WAL-archive (kontinuierlich) + Velero daily, quarterly Restore-Drill
DSGVO-Export <5 minPulumi-Job: kubectl exec pg-cluster -- pg_dump tenant_X → S3 → presigned-link
HAProxy-Switch ohne Connection-Dropingress-nginx soft-reload (built-in, lua-balancer)
Phased Tier-Migration (10% → 100%)ingress-nginx canary-annotations: weight, header, cookie
Auto-Scale on LoadHorizontalPodAutoscaler — 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: v1
kind: Namespace
metadata: { name: tenant-acme }
---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
namespace: cnpg-system
name: tenant-acme
spec:
cluster: { name: shared-postgres }
name: tenant_acme
owner: tenant_acme
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: tenant-acme
name: app
spec:
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_acme

CNPG 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)

TierRAMCPUDisk-PVCDB-Größe
Free256Mi0.25 cpu1 GB500 MB
Starter1 Gi1 cpu10 GB5 GB
Pro4 Gi2 cpu50 GB25 GB
Enterprisecustomcustomcustomcustom

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 restart

Downtime: ~10s (rolling deployment, 1 replica).

Cross-Node-Move (Starter → Pro mit dedicated Node):

  • Tier-config setzt nodeAffinity auf “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:

Terminal window
velero schedule create daily \
--schedule="0 3 * * *" \
--snapshot-volumes \
--include-namespaces tenant-*,cnpg-system

Volumes + 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):

Terminal window
# Pulumi-Provisioner oder cloud-init:
apt install -y wireguard
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
cat > /etc/wireguard/wg0.conf <<'EOF'
[Interface]
Address = 10.10.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
# Marc lokal
[Peer]
PublicKey = <marc-laptop-public-key>
AllowedIPs = 10.10.0.10/32
EOF
systemctl enable --now wg-quick@wg0

Hetzner-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:

~/.config/wireguard/kumiko.conf
[Interface]
Address = 10.10.0.10/24
PrivateKey = <marc-private>
[Peer]
PublicKey = <server-public>
Endpoint = <m1-public-ip>:51820
AllowedIPs = 10.10.0.0/24, 10.0.0.0/16 # auch Cluster-internal-Network
PersistentKeepalive = 25
Terminal window
wg-quick up kumiko # VPN-tunnel aktiv
ssh root@10.10.0.1 # SSH zum Master via VPN
kubectl get nodes # K8s-API erreichbar via VPN

Worker-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.so und *.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.yml

Customer 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.eu auf 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 up rennen (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

  1. K3s-Cluster-Backup — Velero macht PVC + Resources. Etcd-Backup separat? K3s hat eigenen sqlite-state (kein etcd default). k3s etcd-snapshot als CronJob.
  2. 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.
  3. Pulumi-Backend — Pulumi-Cloud (free für solo) vs self-hosted Pulumi-Backend (S3 als state). Solo: Pulumi-Cloud free reicht.
  4. 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.
  5. Tenant-DB-Connection-Pooling — pgbouncer-CRD von CNPG für Tenant-DBs? Bei <50 Tenants nicht nötig.
  6. Image-Registryghcr.io reicht für Phase I.A. Bei Self-Host-Enterprise muss Customer eigene Registry haben oder wir bieten Pull-Token. Einfacher: customer mirror via skopeo 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:

  1. 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.
  2. Multi-Tenant-Operations sind in Coolify Custom-Skripte — gleiches in K8s ist 1 CRD oder 1 HPA-Manifest.
  3. Pulumi vs Coolify-API — Pulumi ist mächtiger, declarative, version-controlled. Coolify-API ist OK aber UI-State-driven.
  4. 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-IDHosting-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