Konfiguration
Diese Seite erklärt das Domain-Modell, die Secrets, TLS-Zertifikate, den Betrieb hinter einem CDN, Network Policies -- und schließt mit der vollständigen Werte-Referenz.
Domain-Modell
Die einzige Pflichtangabe im Chart ist domains.app. Aus ihr leitet das Chart alle ineinandergreifenden Domain-Einstellungen ab (URLs, Session-/Cookie-Scope, Sanctum, CORS, CNAME-Ziele).
| Schlüssel | Beispiel | Wofür |
|---|---|---|
domains.app | monitoring.example.com | Pflicht. Die Domain, unter der das Dashboard und die API erreichbar sind. |
domains.cookieRoot | example.com | Nur, wenn Portale/Status Pages sich den Apex teilen -- setzt den Cookie-Scope übergreifend. |
domains.portalRoot | portal.example.com | Wurzel für White-Label-Kundenportale (<slug>.portal.example.com). |
domains.statusRoot | status.example.com | Wurzel für Status Pages auf Subdomains und das CNAME-Ziel für eigene Kundendomains. |
Für einen einfachen Single-Tenant-Betrieb reicht domains.app allein. portalRoot und statusRoot brauchst du erst, wenn du White-Label-Portale oder Status Pages auf Subdomains anbietest -- siehe White-Label & Reseller.
Secrets
Hoppla self-hosted verwendet drei Secrets im Namespace hoppla:
hoppla-license-- deine signierte Lizenzdatei, gemountet unter/var/hoppla/licenseauf allen PHP-Pods.hoppla-registry-- die Pull-Zugangsdaten für die lizenzgebundene Image-Registry.hoppla-secrets-- das Anwendungs-Secret. Hier liegenAPP_KEY, die Datenbank- und Redis-Zugangsdaten, die Reverb-Schlüssel und dieMAIL_*-Einstellungen.
Beim install.sh werden die Secrets für dich angelegt und Zufallswerte generiert. Beim manuellen Weg legst du sie selbst an:
kubectl create namespace hoppla
# Lizenz
kubectl -n hoppla create secret generic hoppla-license \
--from-file=hoppla.license=license.txt
# Registry-Pull (aus dem Welcome-Pack)
kubectl -n hoppla create secret docker-registry hoppla-registry \
--docker-server=ghcr.io --docker-username=<customer> --docker-password=<token>
# Anwendungs-Secret
kubectl -n hoppla create secret generic hoppla-secrets \
--from-literal=APP_KEY="base64:$(openssl rand -base64 32)" \
--from-literal=DB_USERNAME=hoppla_app \
--from-literal=DB_PASSWORD="$(openssl rand -hex 24)" \
--from-literal=REDIS_PASSWORD="$(openssl rand -hex 24)" \
--from-literal=REVERB_APP_ID="$(openssl rand -hex 8)" \
--from-literal=REVERB_APP_KEY="$(openssl rand -hex 16)" \
--from-literal=REVERB_APP_SECRET="$(openssl rand -hex 24)"Hinweis zu Row-Level Security (RLS)
Die Self-Hosted-Profile setzen rls.enabled: true -- Tenant-Isolation wird vom ersten Tag an auf Datenbankebene erzwungen. Dafür sind der privilegierte Owner und das eingeschränkte Runtime-App-Konto getrennte Identitäten mit getrennten Passwörtern. Das install.sh generiert beide Paare automatisch (DB_USERNAME/DB_PASSWORD = hoppla_app, plus DB_USERNAME_PRIVILEGED/DB_PASSWORD_PRIVILEGED für Migrationen). Beim manuellen Weg orientierst du dich am Kommentar in install.sh.
SMTP-Schlüssel
Die MAIL_*-Schlüssel leben ebenfalls auf hoppla-secrets: MAIL_MAILER, MAIL_HOST, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_ENCRYPTION, MAIL_FROM_ADDRESS. Vollständiges Beispiel und die Encryption-Regeln stehen in der Installation → E-Mail (SMTP).
Secrets patchen und Pods neu rollen
Pods lesen die Secrets beim Start. Nach jeder Änderung musst du die Pods neu rollen, damit die neuen Werte greifen:
kubectl -n hoppla patch secret hoppla-secrets --type merge -p '{"stringData":{ ... }}'
kubectl -n hoppla rollout restart deployDenk daran: Benachrichtigungen verschicken die Worker-Pods. Bei Mailer-Änderungen also rollout restart deploy (alle Deployments), nicht nur die API.
TLS & Zertifikate
cert-manager stellt deine Zertifikate automatisch aus und erneuert sie. Welcher ACME-Solver passt, hängt von der Art der Domain ab:
- HTTP-01 -- der einfache Standard für einzelne Hosts wie
domains.app. Funktioniert, sobald DNS auf deinen Ingress zeigt. - DNS-01 -- nötig für zweistufige Wildcards wie
*.status.example.comund*.portal.example.com. Plain HTTP-01 kann für sie keine Zertifikate ausstellen.
*.status und *.portal sind zweistufige Wildcards. Für sie brauchst du entweder einen DNS-01-Issuer oder Per-Host-Zertifikate. Ein Beispiel für einen DNS-01-ClusterIssuer mit Cloudflare-Token findest du im nächsten Abschnitt.
Betrieb hinter Cloudflare (oder einem anderen CDN)
Hoppla Cloud läuft hinter Cloudflare, und dasselbe Muster funktioniert self-hosted sogar auf dem kostenlosen Cloudflare-Plan. Die nicht offensichtlichen Teile:
Was proxien, was nur als DNS lassen
- Proxien (orange Wolke) deine eigenen Flächen --
domains.app, den Status-/Portal-Apex und eine einstufige Wildcard*.example.com. Cloudflares kostenloses Universal SSL deckt den Apex + eine Wildcard-Ebene ab, diese bekommen also automatisch ein Edge-Zertifikat. - Nur DNS lassen (graue Wolke):
- zweistufige Wildcards wie
*.status.example.com-- kostenloses Universal SSL deckt sie nicht ab, ein Proxy würde einen Zertifikatsfehler ausliefern; - das CNAME-Ziel, auf das Kunden zeigen (
cname.<statusRoot>,<portalRoot>). Das ist der wichtige Punkt -- siehe die Invariante unten.
- zweistufige Wildcards wie
TLS-Einstellungen (wie bei Hoppla Cloud)
- SSL/TLS-Modus:
Full. Verschlüsselt Cloudflare→Origin. Verwende nichtFlexible(Redirect-Schleifen, kaputte Cookies/Sanctum).Full (Strict)funktioniert ebenfalls, sobald cert-manager zuverlässig gültige Let's-Encrypt-Zertifikate für jeden geproxyten Host ausstellt -- es ist aber nicht der Startpunkt: Strict blockiert die erste HTTP-01-Ausstellung (der Origin hat noch kein Zertifikat) und weist grau geroutete eigene Domains ab. Starte mitFull, härte später aufFull (Strict), wenn du willst. - Always Use HTTPS: aus. App/Ingress leiten bereits um; aus zu lassen vermeidet Schleifen unter
Fullund fängt nie ACME-HTTP-01-Challenges ab. - TLS 1.3 an, min. TLS 1.0+ -- die Defaults passen.
Zertifikate: DNS-01 für deine eigenen Wildcards
Hinter einem Proxy erreicht HTTP-01 den Origin für geproxyte Hosts nicht und kann gar keine Wildcards ausstellen. Stelle deine eigenen Wildcard-/Proxy-Zertifikate mit einem DNS-01-ClusterIssuer aus, der von einem Cloudflare-API-Token getragen wird (Scope: Zone → DNS → Edit auf deiner Zone), und behalte einen HTTP-01-Solver als Fallback für graue/eigene Kundendomains:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-prod }
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef: { name: letsencrypt-prod-account-key }
solvers:
- dns01: # deine eigenen Wildcards + geproxyte Hosts
cloudflare:
apiTokenSecretRef: { name: cloudflare-api-token, key: api-token }
selector: { dnsZones: ["example.com"] }
- http01: # graue / externe Kundendomains
ingress: { class: nginx }(kubectl -n cert-manager create secret generic cloudflare-api-token --from-literal=api-token=<token>.)
Echte Client-IPs
Hinter dem Proxy sieht dein Cluster die IPs von Cloudflare. Setze auf ingress-nginx use-forwarded-headers: "true" und compute-full-forwarded-for: "true" und vertraue über proxy-real-ip-cidr nur den veröffentlichten Cloudflare-Bereichen -- sonst ist X-Forwarded-For fälschbar und das Rate-Limiting/Audit-Log der App sieht falsche IPs.
WebSockets
Live-Dashboard-Updates laufen über WebSockets (Reverb). Cloudflare proxyt WSS standardmäßig -- füg nur keine Cache-Regel hinzu, die den /app-Pfad schluckt.
Edge-Cases (die, die wirklich beißen)
- Proxye niemals das CNAME-Ziel, auf das Kunden zeigen -- auf keinem Plan. Ein
status.client.com, das per CNAME auf ein geproxytescname.<statusRoot>zeigt, löst auf deinen Cloudflare-Edge auf, aber dieser Hostname ist nicht in deinem Cloudflare-Account → CF gibt Error 1014 / einen SSL-Fehler zurück, und die HTTP-01-Challenge von cert-manager erreicht deinen Origin ebenfalls nicht. Halte dieses Ziel grau, damit externe Domains den Origin direkt treffen und ein Zertifikat bekommen. - Bezahltes ACM / Total TLS ändert daran nichts. Total TLS erlaubt dir, deine eigenen zweistufigen Wildcards mit Edge-Zertifikaten zu proxien (schön, additiv), und
Full (Strict)passt für deine eigenen geproxyten Hosts -- aber Regel (1) gilt für externe Kundendomains weiterhin. Um externe Kundendomains auf Cloudflare am Edge zu terminieren, brauchst du Cloudflare for SaaS / Custom Hostnames (ein separates Produkt); es ist optional -- der Weg „graue Wolke + Origin-cert-manager" funktioniert auf jedem Plan. - Der Endkunde ist selbst auf Cloudflare und proxyt seine eigene Status-Domain. Dann fängt sein Cloudflare die HTTP-01-Challenge ab und dein cert-manager kann sie nicht validieren. Lösung: Er lässt den Eintrag grau, bis das Zertifikat ausgestellt ist, oder liefert sein eigenes Origin-Zertifikat. (Deine eigenen Domains betrifft das nie -- die nutzen DNS-01 über deinen Token.)
Andere CDNs
Dasselbe Muster gilt für Fastly, Bunny usw.: Der Origin muss ein gültiges Zertifikat präsentieren (DNS-01 oder eines hochladen), die echte Client-IP über den Header des CDN weiterreichen (nur den Bereichen des CDN vertrauen), volle/verschlüsselte Origin-TLS verwenden (nicht „flexible") und WebSocket-Upgrades erlauben.
Network Policies
Netzwerk-Isolation funktioniert auf jedem CNI:
networkPolicy:
enabled: true
backend: standard # der cilium-Backend ist verfügbar, wenn du Cilium fährstBei networkPolicy.enabled: true werden je Komponente feingranulare Ingress-/Egress-Regeln angewendet. PostgreSQL und Redis bekommen eigene Policies, die den Zugriff auf die App-Pods beschränken.
Werte-Referenz
Die Self-Hosted-Profile (values.self-hosted-*.yaml) überlagern die Chart-Defaults. Im Folgenden die wichtigsten Schlüssel; die Profile selbst sind durchgehend kommentiert.
Global
| Schlüssel | Typ | Default | Beschreibung |
|---|---|---|---|
global.imageTag | string | "" | Image-Tag für alle Komponenten überschreiben |
global.imagePullPolicy | string | IfNotPresent | Image-Pull-Policy |
global.imagePullSecrets | list | [] | Referenz auf das Registry-Pull-Secret (hoppla-registry) |
Edition & Domains
| Schlüssel | Typ | Default | Beschreibung |
|---|---|---|---|
config.HOPPLA_EDITION | string | self_hosted | Edition-Schalter -- Entitlements kommen aus der Lizenz |
config.APP_ENV | string | production | Anwendungsumgebung |
config.REGISTRATION_ENABLED | string | "false" | Invite-only; auf "true" für Self-Signup |
domains.app | string | "" | Pflicht -- siehe Domain-Modell |
license.secretName | string | hoppla-license | Name des Lizenz-Secrets |
existingSecret | string | "" | Verweist auf das vorgefertigte hoppla-secrets |
API & Serving-Schicht
| Schlüssel | Typ | Default | Beschreibung |
|---|---|---|---|
api.replicaCount | int | 1 | Replicas (ignoriert, wenn HPA aktiv) |
api.hpa.enabled | bool | false | HPA aktivieren |
api.migration.enabled | bool | true | Migrationen als Init-Container ausführen |
dashboard.replicaCount | int | 1 | Dashboard-Replicas |
reverb.replicaCount | int | 1 | WebSocket-Server (Reverb); bei >1 werden Sticky-Sessions automatisch gesetzt |
scheduler.replicaCount | int | 1 | Niemals skalieren -- ein einziger Dispatcher per Design |
Workers
Workers sind als Map definiert; jeder Eintrag erzeugt ein Deployment mit eigener Queue (uptime, uptime-verify, dns, ssl, alerts, crawl, default, …). Beispiel aus dem Medium-Profil:
workers:
uptime:
replicaCount: 2
env:
PROBE_LOCATION: primary
UPTIME_BATCH_SIZE: "150"
hpa:
enabled: true
minReplicas: 2
maxReplicas: 8
targetCPUUtilizationPercentage: 60
uptime-verify:
replicaCount: 1
env:
PROBE_LOCATION: secondaryWelche Stellschraube du in welcher Reihenfolge anfasst, steht in Skalierung & Betreiber → Skalierungs-Stellschrauben.
PostgreSQL (im Cluster)
| Schlüssel | Typ | Default | Beschreibung |
|---|---|---|---|
postgresql.enabled | bool | true | TimescaleDB-StatefulSet deployen |
postgresql.persistence.size | string | 10Gi | PVC-Größe |
postgresql.persistence.storageClass | string | "" | z. B. gp3 (EKS) / premium-rwo (GKE); leer = Cluster-Default |
Externe Datenbank
Bei postgresql.enabled: false zeigst du config.DB_HOST auf deine eigene Datenbank -- die TimescaleDB unterstützen muss (RDS/Cloud SQL tun das nicht; siehe Skalierung & Betreiber).
Redis (im Cluster)
| Schlüssel | Typ | Default | Beschreibung |
|---|---|---|---|
redis.enabled | bool | true | Redis-StatefulSet deployen |
redis.maxmemory | string | - | Queue-Speicher |
redis.resources.limits.memory | string | - | Invariante: >= 2 × maxmemory + 64Mi (BGSAVE-Fork-Reserve; das Chart verweigert Verstöße) |
Ingress, RLS & Network Policy
| Schlüssel | Typ | Default | Beschreibung |
|---|---|---|---|
ingress.enabled | bool | false | Ingress-Resource erstellen -- siehe Installation |
ingress.className | string | nginx | Ingress-Klasse |
rls.enabled | bool | true | Row-Level-Security-Durchsetzung |
networkPolicy.enabled | bool | true | Per-Komponente-Ingress/Egress-Regeln |
customDomains.provisioner | string | ingress | ingress (API erstellt Ingress+Cert selbst) oder manual (Betreiber-Anweisungen) |
certManager.clusterIssuer | string | letsencrypt-prod | Der ClusterIssuer, auf den das Chart verweist |