Configuration
How to configure your instance after install: the domains model, secrets, TLS, running behind a CDN, network policies, and the full Helm values reference.
Domains
The chart is built around a single required value, domains.app. Everything else derives from it.
| Key | Example | What it controls |
|---|---|---|
domains.app | monitoring.example.com | Required. The app URL, session/cookie scope, Sanctum stateful domains, CORS, and the default CNAME targets. |
domains.cookieRoot | example.com | Only set this when portals or status pages share the apex with the app and need a shared cookie scope. |
domains.portalRoot | portal.example.com | The root for white-label reseller portals (<slug>.portal.example.com). |
domains.statusRoot | status.example.com | The root for status pages and the CNAME target customers point their own status domains at. |
For a basic single-tenant install, domains.app alone is enough. You only need portalRoot / statusRoot once you publish status pages on their own subdomain or run reseller portals (see White-label).
Secrets
All sensitive configuration lives on the hoppla-secrets Secret, referenced via existingSecret: hoppla-secrets. The installer generates most of it; you supply SMTP and, optionally, external DB/Redis credentials.
| Key | Purpose |
|---|---|
APP_KEY | Laravel app encryption key (generated). |
DB_USERNAME / DB_PASSWORD | The restricted runtime role (hoppla_app — no superuser, no RLS bypass). Every pod connects with this so tenant isolation is enforced. |
DB_RLS_APP_PASSWORD | Must equal DB_PASSWORD (same role). The RLS-roles job uses it to provision the runtime role. |
DB_USERNAME_PRIVILEGED / DB_PASSWORD_PRIVILEGED | The owner identity, used only by the migration init container (migrations need ownership: hypertables, compression, policies). A distinct password from the runtime role. |
REDIS_PASSWORD | Redis auth (generated). |
REVERB_APP_ID / REVERB_APP_KEY / REVERB_APP_SECRET | WebSocket (live dashboard) keys (generated). The dashboard's public REVERB_APP_KEY must match — see dashboard runtime config. |
MAIL_MAILER / MAIL_HOST / MAIL_PORT / MAIL_USERNAME / MAIL_PASSWORD / MAIL_ENCRYPTION / MAIL_FROM_ADDRESS | SMTP. |
TURNSTILE_SECRET_KEY | Captcha secret, if you enable any registration surface. |
Patching a secret and rolling pods
Pods read secrets at boot, so any change requires a restart:
kubectl -n hoppla patch secret hoppla-secrets --type merge -p '{"stringData":{
"MAIL_PASSWORD":"<new value>"
}}'
kubectl -n hoppla rollout restart deployRoll all deployments, not just the API — workers send the alerts. (See the warning in Installation → Email.)
TLS & certificates
cert-manager issues and renews everything automatically once your ClusterIssuer is in place.
- The app domain and any single host issue fine with a plain HTTP-01 solver.
- Two-level wildcards —
*.status.example.com,*.portal.example.com— cannot be issued by HTTP-01. Use a DNS-01 issuer, or per-host certificates.
A DNS-01 issuer (here, Cloudflare) for your wildcards, keeping HTTP-01 as the fallback for grey-routed / external custom domains:
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: # your own wildcards + proxied hosts
cloudflare:
apiTokenSecretRef: { name: cloudflare-api-token, key: api-token }
selector: { dnsZones: ["example.com"] }
- http01: # grey / external custom domains
ingress: { class: nginx }Create the token secret (scope: Zone → DNS → Edit on your zone):
kubectl -n cert-manager create secret generic cloudflare-api-token \
--from-literal=api-token=<token>Check certificate health any time with kubectl -n hoppla get certificate — everything should read READY.
Running behind Cloudflare (or another CDN)
The same pattern works on a free Cloudflare plan. These are the non-obvious parts.
What to proxy, what to leave grey
- Proxy (orange-cloud) your own surfaces —
domains.app, the status/portal apex, and a single-level wildcard*.example.com. Free Universal SSL covers the apex plus one wildcard level, so these get an edge cert automatically. - Leave DNS-only (grey-cloud):
- two-level wildcards like
*.status.example.com— Universal SSL does not cover them, so proxying would serve a cert error; - the CNAME target customers point at (
cname.<statusRoot>,<portalRoot>) — this is the important one; see the edge cases below.
- two-level wildcards like
TLS mode
- SSL/TLS mode:
Full. This encrypts Cloudflare → origin. Do not useFlexible(it causes redirect loops and breaks cookies/Sanctum).Full (Strict)also works once cert-manager is reliably issuing valid certs for every proxied host, but it is not the starting point — Strict deadlocks the first HTTP-01 issuance (the origin has no cert yet) and rejects grey-routed custom domains. Start onFull, harden later. - Always Use HTTPS: off. The app/ingress already redirects; leaving it off avoids loops under
Fulland never intercepts ACME HTTP-01 challenges. - TLS 1.3 on, min TLS 1.0+ — the defaults are fine.
Certificates behind the proxy
Behind a proxy, HTTP-01 can't reach the origin for proxied hosts and can't issue wildcards at all — use the DNS-01 issuer above for your own wildcards and proxied hosts, with HTTP-01 as the fallback for grey/external domains.
Real client IPs
Behind the proxy your cluster sees Cloudflare's IPs. On ingress-nginx set use-forwarded-headers: "true" and compute-full-forwarded-for: "true", and trust only Cloudflare's published ranges via proxy-real-ip-cidr — otherwise X-Forwarded-For is spoofable and rate-limiting / audit logs record the wrong IP.
WebSockets
Live dashboard updates use WebSockets (Reverb). Cloudflare proxies WSS by default — just don't add a cache rule that swallows the /app path.
Custom-domain edge cases (these are the ones that bite)
- Never proxy the CNAME target customers point at — on any plan. A customer's
status.client.comCNAME'd to a proxiedcname.<statusRoot>resolves to your Cloudflare edge, but that hostname isn't in your account → Cloudflare returns error 1014 / an SSL failure, and cert-manager's HTTP-01 challenge can't reach your origin either. Keep that target grey so external domains hit the origin directly and get a cert. - Paid ACM / Total TLS does not change this. Total TLS lets you proxy your own two-level wildcards with edge certs (nice, additive) and
Full (Strict)is fine for your own proxied hosts — but rule (1) still holds for external customer domains. To edge-terminate external customer domains you need Cloudflare for SaaS / Custom Hostnames (a separate, optional product); the grey-cloud + origin cert-manager path works on every plan. - The customer is themselves on Cloudflare and proxies their own status domain. Then their Cloudflare intercepts the HTTP-01 challenge and your cert-manager can't validate it. Fix: they grey-cloud the record until the cert issues, or supply their own origin certificate.
Other CDNs
The same shape applies to Fastly, Bunny, etc.: the origin must present a valid cert (DNS-01, or upload one), forward the real client IP via the CDN's header (trust only the CDN's ranges), use full/encrypted origin TLS (not "flexible"), and allow WebSocket upgrades.
Network policies
The chart applies fine-grained ingress/egress rules per component when network policies are enabled — PostgreSQL and Redis get their own policies restricting access to the app pods only.
networkPolicy:
enabled: true
backend: standard # works on any CNI; use 'cilium' only when you run Ciliumbackend: standard works on any CNI. The cilium backend is available when you run Cilium.
Values reference
The self-hosted profiles (Scaling → Choosing a profile) overlay these chart defaults. The most-used keys:
Global
| Key | Default | Description |
|---|---|---|
global.imageTag | "" | Override image tag for all components. |
global.imagePullPolicy | IfNotPresent | Image pull policy. |
global.imagePullSecrets | [] | Pull-secret references (set to hoppla-registry). |
API
| Key | Default | Description |
|---|---|---|
api.replicaCount | 1 | Replicas (ignored when HPA is enabled). |
api.hpa.enabled | false | Enable horizontal autoscaling. |
api.migration.enabled | true | Run migrations as an init container before the API starts. |
api.env | {} | Per-pod env (branding, PROBE_LOCATION, etc.). |
Workers
Workers are a map; each entry becomes its own Deployment consuming specific queues.
workers:
default: { replicaCount: 1, queues: ["default"] }
uptime: { replicaCount: 2, queues: ["uptime"] }
uptime-verify: { replicaCount: 1, queues: ["uptime-verify"] } # the dual-check second opinion
dns: { replicaCount: 1, queues: ["dns"] }
ssl: { replicaCount: 1, queues: ["ssl"] }
alerts: { replicaCount: 1, queues: ["alerts", "notifications"] }
crawl: { replicaCount: 1, queues: ["crawl"] }Each pool takes replicaCount, an optional hpa block, and per-pool env. Never scale the scheduler or run the alerts pool in more than one cluster — see Scaling knobs.
PostgreSQL (in-cluster)
| Key | Default | Description |
|---|---|---|
postgresql.enabled | true | Deploy the TimescaleDB StatefulSet. Set false to bring your own (read the TimescaleDB constraint first). |
postgresql.persistence.size | 10Gi | PVC size. |
postgresql.persistence.storageClass | "" | e.g. gp3 (EKS), premium-rwo (GKE); empty = cluster default. |
postgresql.resources | — | Requests/limits; prefer faster disks over more CPU. |
Redis (in-cluster)
| Key | Default | Description |
|---|---|---|
redis.enabled | true | Deploy the Redis StatefulSet. |
redis.maxmemory | — | Queue/cache budget. Invariant: resources.limits.memory >= 2 × maxmemory + 64Mi (BGSAVE fork headroom — the chart refuses to render violations). |
redis.persistence.enabled | false | Redis holds queues + cache, not durable data; no backup needed. |
Backups
| Key | Default | Description |
|---|---|---|
backups.enabled | false | Enable the pg_dump CronJob. |
backups.schedule | 0 2 * * * | Cron schedule. |
backups.retentionDays | 30 | Days to retain. |
backups.persistence.size | 20Gi | Backup PVC size. |
Ingress
| Key | Default | Description |
|---|---|---|
ingress.enabled | false | Create the Ingress (off by default — see Exposing over HTTPS). |
ingress.className | nginx | Ingress class. |
ingress.tls | [] | TLS hosts/secret. |
When reverb.replicaCount > 1, sticky-session annotations are added automatically for WebSocket affinity.
Custom domains & cert-manager
| Key | Default | Description |
|---|---|---|
certManager.clusterIssuer | letsencrypt-prod | The issuer the chart references. |
customDomains.provisioner | ingress | ingress = the API creates the Ingress + Certificate itself; manual = it returns operator instructions for you to wire up. |
portalDomains.provisioner | ingress | Same, for reseller portal domains. |
Secrets
existingSecret: hoppla-secrets # reference a pre-created Secret instead of inline credentials
license:
secretName: hoppla-license