Skip to content

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üsselBeispielWofür
domains.appmonitoring.example.comPflicht. Die Domain, unter der das Dashboard und die API erreichbar sind.
domains.cookieRootexample.comNur, wenn Portale/Status Pages sich den Apex teilen -- setzt den Cookie-Scope übergreifend.
domains.portalRootportal.example.comWurzel für White-Label-Kundenportale (<slug>.portal.example.com).
domains.statusRootstatus.example.comWurzel 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/license auf allen PHP-Pods.
  • hoppla-registry -- die Pull-Zugangsdaten für die lizenzgebundene Image-Registry.
  • hoppla-secrets -- das Anwendungs-Secret. Hier liegen APP_KEY, die Datenbank- und Redis-Zugangsdaten, die Reverb-Schlüssel und die MAIL_*-Einstellungen.

Beim install.sh werden die Secrets für dich angelegt und Zufallswerte generiert. Beim manuellen Weg legst du sie selbst an:

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

bash
kubectl -n hoppla patch secret hoppla-secrets --type merge -p '{"stringData":{ ... }}'
kubectl -n hoppla rollout restart deploy

Denk 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.com und *.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.

TLS-Einstellungen (wie bei Hoppla Cloud) ​

  • SSL/TLS-Modus: Full. Verschlüsselt Cloudflare→Origin. Verwende nicht Flexible (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 mit Full, härte später auf Full (Strict), wenn du willst.
  • Always Use HTTPS: aus. App/Ingress leiten bereits um; aus zu lassen vermeidet Schleifen unter Full und 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:

yaml
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) ​

  1. Proxye niemals das CNAME-Ziel, auf das Kunden zeigen -- auf keinem Plan. Ein status.client.com, das per CNAME auf ein geproxytes cname.<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.
  2. 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.
  3. 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:

yaml
networkPolicy:
  enabled: true
  backend: standard   # der cilium-Backend ist verfügbar, wenn du Cilium fährst

Bei 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üsselTypDefaultBeschreibung
global.imageTagstring""Image-Tag für alle Komponenten überschreiben
global.imagePullPolicystringIfNotPresentImage-Pull-Policy
global.imagePullSecretslist[]Referenz auf das Registry-Pull-Secret (hoppla-registry)

Edition & Domains ​

SchlüsselTypDefaultBeschreibung
config.HOPPLA_EDITIONstringself_hostedEdition-Schalter -- Entitlements kommen aus der Lizenz
config.APP_ENVstringproductionAnwendungsumgebung
config.REGISTRATION_ENABLEDstring"false"Invite-only; auf "true" für Self-Signup
domains.appstring""Pflicht -- siehe Domain-Modell
license.secretNamestringhoppla-licenseName des Lizenz-Secrets
existingSecretstring""Verweist auf das vorgefertigte hoppla-secrets

API & Serving-Schicht ​

SchlüsselTypDefaultBeschreibung
api.replicaCountint1Replicas (ignoriert, wenn HPA aktiv)
api.hpa.enabledboolfalseHPA aktivieren
api.migration.enabledbooltrueMigrationen als Init-Container ausführen
dashboard.replicaCountint1Dashboard-Replicas
reverb.replicaCountint1WebSocket-Server (Reverb); bei >1 werden Sticky-Sessions automatisch gesetzt
scheduler.replicaCountint1Niemals 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:

yaml
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: secondary

Welche Stellschraube du in welcher Reihenfolge anfasst, steht in Skalierung & Betreiber → Skalierungs-Stellschrauben.

PostgreSQL (im Cluster) ​

SchlüsselTypDefaultBeschreibung
postgresql.enabledbooltrueTimescaleDB-StatefulSet deployen
postgresql.persistence.sizestring10GiPVC-Größe
postgresql.persistence.storageClassstring""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üsselTypDefaultBeschreibung
redis.enabledbooltrueRedis-StatefulSet deployen
redis.maxmemorystring-Queue-Speicher
redis.resources.limits.memorystring-Invariante: >= 2 × maxmemory + 64Mi (BGSAVE-Fork-Reserve; das Chart verweigert Verstöße)

Ingress, RLS & Network Policy ​

SchlüsselTypDefaultBeschreibung
ingress.enabledboolfalseIngress-Resource erstellen -- siehe Installation
ingress.classNamestringnginxIngress-Klasse
rls.enabledbooltrueRow-Level-Security-Durchsetzung
networkPolicy.enabledbooltruePer-Komponente-Ingress/Egress-Regeln
customDomains.provisionerstringingressingress (API erstellt Ingress+Cert selbst) oder manual (Betreiber-Anweisungen)
certManager.clusterIssuerstringletsencrypt-prodDer ClusterIssuer, auf den das Chart verweist