- 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> |
||
|---|---|---|
| .. | ||
| compose | ||
| terraform | ||
| README.md | ||
| update.sh | ||
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)
- Terraform ≥ 1.6 + Hetzner API-Token (Projekt → Security → API Tokens, Read&Write).
- Object Storage in der Hetzner Console anlegen: Bucket + Access-Key/Secret (hcloud/Terraform verwalten Object Storage derzeit nicht).
- Git-Repo erreichbar für die Nodes (öffentlich oder Deploy-Token in der
repo_url). - JWT-Schlüsselpaar einmal erzeugen — auf allen Nodes identisch:
Inhalt der beiden PEM-Dateien + Passphrase in# 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)terraform.tfvarseintragen. - DNS: A-Record
domain → caddy_ip(+ optional*.zone → caddy_ipfür Subdomains). Entweder manuell oder automatisch über die Hetzner DNS API (manage_dns = truehetzner_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_proxiesauf%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:seedaufapp-1fü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):
- Pass Type ID anlegen, Zertifikat erzeugen → als PEM exportieren (
cert.pem+key.pem). - Apple WWDR-Zwischenzertifikat als PEM (
wwdr.pem). - 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):
- In der Google Cloud Console die Wallet API + einen Issuer anlegen, Service-Account mit Rolle „Wallet Object Issuer" erstellen, JSON-Key herunterladen.
- Env setzen:
GOOGLE_WALLET_ISSUER_ID,GOOGLE_WALLET_SERVICE_ACCOUNT(Pfad zur JSON), optionalGOOGLE_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).