vcard4reseller/deploy
Thomas Peterson 8daef8e98f White-Label Phase 5: DNS-Automatik für Firmen-Subdomains
- DnsProvisioner (dependency-frei, cURL) legt pro Reseller *.<slug>.<portal>
  A-Record via Hetzner-Cloud-DNS-API an (deckt firma.reseller.portal ab,
  was der globale *.<portal>-Eintrag nicht kann)
- ResellerDnsListener (Doctrine postPersist/preRemove), fail-soft,
  überspringt Plattform-Reseller
- Env HCLOUD_DNS_TOKEN/HCLOUD_DNS_ZONE_NAME (leer = aus); Terraform reicht
  Cloud-Token + Zone an die App-Nodes durch (nur bei manage_dns)
- Ziel-IP = APP_EDGE_IP oder DNS-Auflösung der Portal-Domain

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 18:14:41 +02:00
..
compose Deploy-Fix: nginx routet /w (Wallet) zum Backend + nginx-Recreate bei Rollout 2026-06-05 09:17:30 +02:00
terraform White-Label Phase 5: DNS-Automatik für Firmen-Subdomains 2026-06-09 18:14:41 +02:00
README.md Deploy: Terraform-Code-Rollout auf App-Nodes (ohne Recreate) 2026-06-04 19:47:59 +02:00
update.sh Deploy-Fix: nginx routet /w (Wallet) zum Backend + nginx-Recreate bei Rollout 2026-06-05 09:17:30 +02:00

Deployment auf Hetzner Cloud (Terraform)

Provisioniert einen Multi-Node-Test mit nachgewiesener Skalierbarkeit:

        Internet (HTTPS)
              │
        vcard4-caddy   ── TLS: Portal-Domain (automatisch) + Custom-Domains (On-Demand)
        (Reverse-Proxy + Load-Balancing)
            /        \
     vcard4-app-1   vcard4-app-2     (zustandslose App-Nodes, HTTP im privaten Netz)
            \        /
   ┌─────────┴──────────┐   ┌────────┴───────────────┐
 vcard4-db (MariaDB+Vol)     Hetzner Object Storage (S3, Assets)

Caddy übernimmt TLS (Let's Encrypt) und Load-Balancing — kein Hetzner-LB nötig. Für Custom-Domains der Firmenkunden (§11) macht Caddy On-Demand-TLS und fragt vorher GET /internal/tls-allowed?domain=… in der App (Portal-Domain oder verifizierte Domain aus der DB) → schützt vor unbegrenzten Zertifikatsanfragen.

App-Nodes sind zustandslos (Code + Docker), State liegt in DB (eigene VM) und Object Storage (Hintergrund-PDFs/Schriften). Dadurch beliebig horizontal skalierbar.

Voraussetzungen (einmalig, manuell)

  1. Terraform ≥ 1.6 + Hetzner API-Token (Projekt → Security → API Tokens, Read&Write).
  2. Object Storage in der Hetzner Console anlegen: Bucket + Access-Key/Secret (hcloud/Terraform verwalten Object Storage derzeit nicht).
  3. Git-Repo erreichbar für die Nodes (öffentlich oder Deploy-Token in der repo_url).
  4. JWT-Schlüsselpaar einmal erzeugen — auf allen Nodes identisch:
    # lokal im backend/-Container oder mit openssl
    docker compose exec php php bin/console lexik:jwt:generate-keypair --overwrite
    # → backend/config/jwt/private.pem & public.pem + Passphrase aus .env (JWT_PASSPHRASE)
    
    Inhalt der beiden PEM-Dateien + Passphrase in terraform.tfvars eintragen.
  5. DNS: A-Record domain → caddy_ip (+ optional *.zone → caddy_ip für Subdomains). Entweder manuell oder automatisch über die Hetzner DNS API (manage_dns = true
    • hetzner_dns_token + dns_zone_name; Zone muss bei Hetzner DNS liegen).

Deploy

cd deploy/terraform
cp terraform.tfvars.example terraform.tfvars   # ausfüllen
terraform init
terraform plan
terraform apply
terraform output                               # load_balancer_ip etc.

cloud-init installiert auf jedem App-Node Docker, klont das Repo, schreibt .env.prod.local + JWT-Keys, baut das SPA und startet deploy/compose/docker-compose.prod.yml. Migrationen laufen nur auf app-1. Deploy-Log auf dem Node: /var/log/vcard4-deploy.log.

Skalierbarkeit verifizieren

# 1) Login über die Domain (Caddy → App-Nodes)
TOKEN=$(curl -s -X POST https://<domain>/api/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@vcard4reseller.de","password":"admin"}' | jq -r .token)

# 2) Health über Caddy (round-robin auf beide Nodes)
for i in $(seq 1 6); do curl -s https://<domain>/health; echo; done

# 3) Cross-Node-Beweis: Hintergrund über die Domain hochladen, dann
#    auf BEIDEN Nodes rendern (per SSH)  identische PDFs aus Object Storage:
ssh root@<app-1-ip> 'cd /opt/vcard4 && docker compose -f deploy/compose/docker-compose.prod.yml exec -T php php bin/console app:render-card erika-mustermann'
ssh root@<app-2-ip> 'cd /opt/vcard4 && docker compose -f deploy/compose/docker-compose.prod.yml exec -T php php bin/console app:render-card erika-mustermann'

Skalieren

app_count erhöhen → terraform apply legt neue App-Nodes an. Achtung: die Caddy-Upstreams stehen in der Caddyfile (per cloud-init beim ersten Boot gerendert) — neue Nodes werden nicht automatisch aufgenommen. Optionen: Caddy-Node neu erstellen (terraform taint hcloud_server.caddy && apply) oder Caddyfile auf dem Caddy-Node aktualisieren + docker exec caddy caddy reload. (Später besser: Caddy-Config-Templating per CI oder Service-Discovery.)

Noch offen / Hinweise

  • TLS: erledigt Caddy (Portal automatisch, Custom-Domains On-Demand). Erste Zertifikatsausstellung dauert ein paar Sekunden nach korrektem DNS.
  • Trusted Proxies: für korrekte absolute URLs hinter Caddy framework.trusted_proxies auf %env(TRUSTED_PROXIES)% setzen.
  • Host-basiertes Routing (Custom-Domain → richtige Firmenseite) ist §11-Folgearbeit; Caddy stellt bereits Zertifikate für verifizierte Domains aus.
  • Seed: optional einmalig app:seed auf app-1 für Demo-Daten.
  • Updates: neuen Stand ausrollen = auf den App-Nodes git pull + docker compose ... up -d --build (später per CI/Skript).

Wallet-Pässe (Apple / Google) — optional

Ausführliche Schritt-für-Schritt-Anleitung: docs/WALLET-SETUP.md.

Auf der öffentlichen Profilseite erscheint ein QR „Zur Wallet hinzufügen" (Landing /w/{code}), sobald die Zugangsdaten gesetzt sind. Ohne Konfiguration ist das Feature ausgeblendet.

Apple Wallet (kostenpflichtiger Apple-Developer-Account):

  1. Pass Type ID anlegen, Zertifikat erzeugen → als PEM exportieren (cert.pem + key.pem).
  2. Apple WWDR-Zwischenzertifikat als PEM (wwdr.pem).
  3. PEM-Dateien außerhalb des Webroots ablegen, Env setzen: APPLE_WALLET_PASS_TYPE_ID, APPLE_WALLET_TEAM_ID, APPLE_WALLET_CERT_PATH, APPLE_WALLET_KEY_PATH, APPLE_WALLET_KEY_PASSWORD, APPLE_WALLET_WWDR_PATH, APPLE_WALLET_ORG_NAME.

Google Wallet (kostenlos):

  1. In der Google Cloud Console die Wallet API + einen Issuer anlegen, Service-Account mit Rolle „Wallet Object Issuer" erstellen, JSON-Key herunterladen.
  2. Env setzen: GOOGLE_WALLET_ISSUER_ID, GOOGLE_WALLET_SERVICE_ACCOUNT (Pfad zur JSON), optional GOOGLE_WALLET_CLASS_SUFFIX.

Hinweis: Selbstsignierte Test-Zertifikate erzeugen ein technisch valides .pkpass/JWT, werden aber von Apple/Google nicht akzeptiert — für die Produktion echte Zugangsdaten nötig. Over-the-air-Sync (APNs/Objekt-Patch) ist noch nicht umgesetzt (nur Pass-Erstellung).

Code-Update ausrollen (ohne Neu-Provisionierung)

Nach git push den neuen Code auf die laufenden App-Nodes bringen:

cd deploy/terraform
tofu apply -var deploy_version=$(git rev-parse --short HEAD)

terraform_data.app_deploy (per var.deploy_version getriggert) führt auf jedem App-Node aus: git fetch/reset auf origin/<branch>, SPA neu bauen, Composer/ Autoloader auffrischen, Cache leeren Migrationen + Seed nur auf app-1 (deploy/update.sh). Die Server bleiben erhalten (cloud-init user_data ist via ignore_changes eingefroren; es zählt nur beim Erstboot). Gleiche deploy_version = kein Rollout; neuer Git-SHA = Rollout. SSH-Key: ssh_private_key_path (Default ~/.ssh/vcard4_deploy).