Skip to content

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.

KeyExampleWhat it controls
domains.appmonitoring.example.comRequired. The app URL, session/cookie scope, Sanctum stateful domains, CORS, and the default CNAME targets.
domains.cookieRootexample.comOnly set this when portals or status pages share the apex with the app and need a shared cookie scope.
domains.portalRootportal.example.comThe root for white-label reseller portals (<slug>.portal.example.com).
domains.statusRootstatus.example.comThe 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.

KeyPurpose
APP_KEYLaravel app encryption key (generated).
DB_USERNAME / DB_PASSWORDThe restricted runtime role (hoppla_app — no superuser, no RLS bypass). Every pod connects with this so tenant isolation is enforced.
DB_RLS_APP_PASSWORDMust equal DB_PASSWORD (same role). The RLS-roles job uses it to provision the runtime role.
DB_USERNAME_PRIVILEGED / DB_PASSWORD_PRIVILEGEDThe owner identity, used only by the migration init container (migrations need ownership: hypertables, compression, policies). A distinct password from the runtime role.
REDIS_PASSWORDRedis auth (generated).
REVERB_APP_ID / REVERB_APP_KEY / REVERB_APP_SECRETWebSocket (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_ADDRESSSMTP.
TURNSTILE_SECRET_KEYCaptcha secret, if you enable any registration surface.

Patching a secret and rolling pods ​

Pods read secrets at boot, so any change requires a restart:

bash
kubectl -n hoppla patch secret hoppla-secrets --type merge -p '{"stringData":{
  "MAIL_PASSWORD":"<new value>"
}}'
kubectl -n hoppla rollout restart deploy

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

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:                              # 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):

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

TLS mode ​

  • SSL/TLS mode: Full. This encrypts Cloudflare → origin. Do not use Flexible (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 on Full, harden later.
  • Always Use HTTPS: off. The app/ingress already redirects; leaving it off avoids loops under Full and 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) ​

  1. Never proxy the CNAME target customers point at — on any plan. A customer's status.client.com CNAME'd to a proxied cname.<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.
  2. 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.
  3. 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.

yaml
networkPolicy:
  enabled: true
  backend: standard   # works on any CNI; use 'cilium' only when you run Cilium

backend: 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 ​

KeyDefaultDescription
global.imageTag""Override image tag for all components.
global.imagePullPolicyIfNotPresentImage pull policy.
global.imagePullSecrets[]Pull-secret references (set to hoppla-registry).

API ​

KeyDefaultDescription
api.replicaCount1Replicas (ignored when HPA is enabled).
api.hpa.enabledfalseEnable horizontal autoscaling.
api.migration.enabledtrueRun 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.

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

KeyDefaultDescription
postgresql.enabledtrueDeploy the TimescaleDB StatefulSet. Set false to bring your own (read the TimescaleDB constraint first).
postgresql.persistence.size10GiPVC 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) ​

KeyDefaultDescription
redis.enabledtrueDeploy 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.enabledfalseRedis holds queues + cache, not durable data; no backup needed.

Backups ​

KeyDefaultDescription
backups.enabledfalseEnable the pg_dump CronJob.
backups.schedule0 2 * * *Cron schedule.
backups.retentionDays30Days to retain.
backups.persistence.size20GiBackup PVC size.

Ingress ​

KeyDefaultDescription
ingress.enabledfalseCreate the Ingress (off by default — see Exposing over HTTPS).
ingress.classNamenginxIngress class.
ingress.tls[]TLS hosts/secret.

When reverb.replicaCount > 1, sticky-session annotations are added automatically for WebSocket affinity.

Custom domains & cert-manager ​

KeyDefaultDescription
certManager.clusterIssuerletsencrypt-prodThe issuer the chart references.
customDomains.provisioneringressingress = the API creates the Ingress + Certificate itself; manual = it returns operator instructions for you to wire up.
portalDomains.provisioneringressSame, for reseller portal domains.

Secrets ​

yaml
existingSecret: hoppla-secrets   # reference a pre-created Secret instead of inline credentials
license:
  secretName: hoppla-license