Skip to content

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.

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

2. cert-manager ​

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

3. A ClusterIssuer ​

The profiles reference letsencrypt-prod. Create it once (substitute your own email):

bash
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 } } }]
EOF

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

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

bash
./install.sh

It prompts for your app domain, license file, registry credentials, chart version, and (optionally) SMTP, then:

  1. Creates the namespace (hoppla).
  2. Creates three secrets:
    • hoppla-license — your signed license file, mounted on every PHP pod.
    • hoppla-registry — a docker-registry pull secret for ghcr.io.
    • hoppla-secrets — the application secret. It generates APP_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.
  3. Installs the chart with values.self-hosted-small.yaml and your domains.app.
  4. 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):

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

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

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

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

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

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

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

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