Installation
This page takes you from a fresh cluster to a running Hoppla instance you can log in to. The whole routine is idempotent — every step is safe to re-run after fixing a value.
You will need your license file and registry credentials from your welcome pack, plus kubectl and helm (3.10+) pointed at the target cluster.
Cluster prerequisites
The chart expects an ingress controller, cert-manager, and a ClusterIssuer to already exist (the self-hosted profiles reference a letsencrypt-prod issuer). On a fresh cluster, install them once before you run the installer.
Your cluster also needs outbound internet access for the monitoring workers — an IPv4-only cluster is fine and needs no configuration. If you intend to monitor IPv6 hosts, read Networking & IP Versions before you create the cluster: on GKE and EKS the IP family is fixed at creation and cannot be changed afterwards.
1. ingress-nginx
Its LoadBalancer Service is what gives you the external address for DNS.
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx --create-namespace \
--set controller.service.type=LoadBalancer --wait2. cert-manager
helm repo add jetstack https://charts.jetstack.io
helm upgrade --install cert-manager jetstack/cert-manager \
--namespace cert-manager --create-namespace --set crds.enabled=true --wait3. A ClusterIssuer
The profiles reference letsencrypt-prod. Create it once (substitute your own email):
kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-prod }
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected] # <-- your email
privateKeySecretRef: { name: letsencrypt-prod-account-key }
solvers: [{ http01: { ingress: { class: nginx } } }]
EOFWildcards behind a CDN
If you will run white-label portals or status pages on two-level wildcards (*.status.example.com), or you sit behind Cloudflare, you will want a DNS-01 issuer instead of (or alongside) HTTP-01. See Configuration → TLS & certificates.
4. Find your external address
This is what you point DNS at. IP-based load balancers (GKE, k3s/MetalLB, bare metal) populate .ip; hostname load balancers (AWS NLB/ELB) populate .hostname. This prints whichever exists:
kubectl -n ingress-nginx get svc ingress-nginx-controller \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}{"\n"}'- An IP → create an A record (
monitoring.example.com A <ip>). - A hostname → create a CNAME (
monitoring.example.com CNAME <host>). An apex domain can't be a CNAME — use a Route 53 Alias /external-dns, or use a subdomain. See Scaling → Amazon EKS.
Provider-specific storage classes and ingress notes live in Scaling → Provider notes.
Install with install.sh
The installer that ships in your welcome pack is the fastest path. It is plain kubectl/helm under the hood — read it if you want to know exactly what runs.
./install.shIt prompts for your app domain, license file, registry credentials, chart version, and (optionally) SMTP, then:
- Creates the namespace (
hoppla). - Creates three secrets:
hoppla-license— your signed license file, mounted on every PHP pod.hoppla-registry— a docker-registry pull secret forghcr.io.hoppla-secrets— the application secret. It generatesAPP_KEY, the Reverb keys, the Redis password, and two distinct database identities: a privileged owner used only by the migration init container, and a restricted runtime role (hoppla_app, no superuser, no RLS bypass) that every pod connects as so row-level tenant isolation is enforced from day one.
- Installs the chart with
values.self-hosted-small.yamland yourdomains.app. - Prints the two finishing commands below.
Re-running is safe
Existing secrets are left untouched — delete one first if you want it regenerated. hoppla:install refuses to touch a non-empty instance.
Finishing commands
Run these inside the API pod (the installer prints the exact pod name — the release fullname is <release>-<chart>-api, not hoppla-api):
# 1. Create your instance and its first admin (no-op if already installed)
kubectl -n hoppla exec -it deploy/hoppla-hoppla-selfhosted-api -c api -- \
php artisan hoppla:install
# 2. End-to-end verification: DB/TimescaleDB, Redis, queues, scheduler, SMTP, RLS, license
kubectl -n hoppla exec -it deploy/hoppla-hoppla-selfhosted-api -c api -- \
php artisan hoppla:doctorThen point DNS for your app domain at the ingress address and log in.
Manual install
If you would rather drive Helm yourself, pre-create the secrets (use install.sh as a reference for the keys, especially the two DB roles), then install the OCI chart with a profile:
helm upgrade --install hoppla \
oci://ghcr.io/jkbennemann/charts/hoppla-selfhosted \
--version <CHART_VERSION> -n hoppla --create-namespace \
-f values.self-hosted-small.yaml \
--set domains.app=monitoring.example.com \
--set existingSecret=hoppla-secrets \
--set license.secretName=hoppla-licenseThe only required value is domains.app. From it the chart derives every interlocking domain setting — app URL, session/cookie scope, Sanctum, CORS, and the CNAME targets. White-label portal and status-page domains come from domains.portalRoot / domains.statusRoot (see Configuration).
Keep exactly one values file per cluster under version control and always pass it with -f. Never use --reuse-values — it freezes chart-default changes and produces drift that is miserable to debug.
Exposing the app over HTTPS
domains.app configures URLs and CORS but does not by itself create an Ingress — ingress.enabled defaults to false, so out of the box the app is reachable only via kubectl port-forward. To serve it on your domain with a cert-manager certificate, add an ingress override and pass it to the installer (HELM_EXTRA_ARGS="-f ingress.yaml" ./install.sh) or to your own helm upgrade:
# ingress.yaml
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
- secretName: hoppla-app-tls
hosts: [monitoring.example.com] # = your domains.app
hosts:
- host: monitoring.example.com # = your domains.app
paths:
- { path: /api, service: api }
- { path: /sanctum, service: api }
- { path: /ping, service: api }
- { path: /app, service: reverb }
- { path: /, service: dashboard }Then point DNS for domains.app at the ingress external address from the prerequisites step. The certificate issues automatically once DNS resolves.
Email (SMTP)
Alerts, invitations, and reports all need a real mailer. Configure it at install (the installer prompts for SMTP) or later by patching the secret.
The mail keys live on hoppla-secrets. Example for Mailgun (EU):
kubectl -n hoppla patch secret hoppla-secrets --type merge -p '{"stringData":{
"MAIL_MAILER":"smtp",
"MAIL_HOST":"smtp.eu.mailgun.org",
"MAIL_PORT":"587",
"MAIL_USERNAME":"[email protected]",
"MAIL_PASSWORD":"<your smtp password>",
"MAIL_ENCRYPTION":"",
"MAIL_FROM_ADDRESS":"[email protected]"
}}'
kubectl -n hoppla rollout restart deployPort 465 is implicit TLS (MAIL_ENCRYPTION=tls); 587/2525/25 use STARTTLS (leave MAIL_ENCRYPTION empty).
Roll every deployment, not just the API
Pods read the secret at boot, so you must roll them all (rollout restart deploy). Alerts are sent by the worker pods, not the API pod — if you restart only the API, the test below will pass (it runs in the API pod) while real alerts keep using the stale mailer.
Verify deliverability:
kubectl -n hoppla exec deploy/hoppla-api -c api -- \
php artisan hoppla:doctor [email protected]hoppla:doctor also warns if the SMTP host is unset or unreachable. Resellers can additionally send from their own domain — see White-label → Per-reseller SMTP.