vcard4reseller/deploy/README.md
Thomas Peterson 79e996ab03 Deployment: Caddy-Edge (TLS + On-Demand für Custom-Domains) + Hetzner DNS
- Caddy ersetzt den Hetzner-LB: terminiert TLS (Portal-Domain automatisch) und
  load-balanced per reverse_proxy über die App-Nodes. Für Custom-Domains (§11)
  On-Demand-TLS, autorisiert über GET /internal/tls-allowed.
- TlsCheckController + DomainRepository::findVerifiedByHostname: erlaubt Zertifikate
  nur für Portal-Domain oder verifizierte Domains (Schutz vor Cert-Flooding).
- Terraform: hcloud_load_balancer entfernt, Caddy-Server + Firewall (80/443) +
  cloud-init-caddy (Caddyfile templated mit Upstreams/Domain/ACME).
- Optional Hetzner DNS via API (manage_dns): A-Record Portal + Wildcard → Caddy.
- nginx.prod: /internal zu Symfony geroutet; APP_PORTAL_DOMAIN-Env.

Validiert: Caddyfile (caddy validate), Terraform (validate), /internal/tls-allowed (200/403/400).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:13:29 +02:00

94 lines
4.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```bash
# 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
```bash
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
```bash
# 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).