Skip to main content
apowerb-chart on Artifact Hub The chart is listed on Artifact Hub, with its values schema, its versions and its cosign signature. The chart deploys the backend, the interface and PostgreSQL, plus the three services the Compose stack runs — each behind a switch — and a volume for what the backend writes to disk.

Before you start

The cluster must be ready first — a default storage class, an ingress controller and a certificate issuer. The Kubernetes page prepares it with kubectl and plain manifests; come back here once its checks pass. You also need helm 3.8 or later, for OCI registry support.

1. Secrets

Five values. Four of them are mandatory — the chart refuses to install without postgres.password and backend.env.encryptKey (no default, on purpose) and the two th2pulse tokens, because the backend and th2pulse do not start without them.
Keep this file, out of version control, and pass it to every later helm upgrade. Without it the Secret is rewritten with empty strings — and a changed ENCRYPT_KEY makes every connected integration unreadable, since it is the key their OAuth tokens were encrypted with. See Trap 2.

2. Install

Start without an Ingress: fewer moving parts, and a public name is not needed to know whether the stack runs.
No --version: Helm installs the newest chart published on Docker Hub, so this page never lags behind a release. helm -n apowerb list shows the version you got; pass it as --version to any later command that must stay on it. Pulling the images takes a few minutes on a fresh cluster — the backend image alone is around 250 MB. From 0.4.22 the published chart is signed with cosign, keyless, by the publishing workflow. To check that a chart comes from apowerb/apowerb-hosting:
The chart moved to its own Docker Hub repository. It used to be pushed to apowerb/apowerb, alongside the backend images — a repository cannot be subdivided there, and helm push names it after the chart. Versions 0.2.0 and 0.4.0 stay where they were published; nothing was deleted. Anythingfrom 0.4.1 on lives at apowerb/apowerb-chart.
First real install: OVH Managed Kubernetes (3 nodes, Cinder ext4 volumes) on 29/09/26. It found two blockers — PostgreSQL failing on the volume’s lost+found, and upgrades stuck on Multi-Attach — both fixed in 0.4.21.

3. Check what actually runs

What a healthy install looks like:
  • six pods Running: backend, frontend, postgres, th2etl, th2pulse, otel-collector;
  • apowerb-th2etl-seed Completed — it is a Job, so 0/1 Completed is the success state, not a failure;
  • two PVCs Bound: the data volume and the database’s.
A couple of restarts on th2etl and th2pulse at first start are expected — Trap 1. Then reach the services:
401 on /api/config/setup is the right answer: authentication comes before the checklist. A 404 there would mean the backend image predates it. These checks go through a port-forward on purpose. The backend is never published directly: once the site is public, the frontend proxies /api/* to it, and nothing else reaches it — /health included (Trap 8).
The checklist reports what is configured, never what is reachable. A capability can read configured: true while the provider refuses every call — see Trap 6. The only proof that a model answers is a real run.

4. Going public

Point an A record at the EXTERNAL-IP of the ingress controller (Kubernetes, step 3), wait for it to resolve, then upgrade with the Ingress — with the staging issuer first (Kubernetes, step 4):
tlsEnabled writes the Ingress tls section; the secret name is derived (apowerb-tls) unless ingress.tlsSecretName names one, which is what cert-manager fills from the annotation above.
Left at false, the interface is served in clear — and the address verification links it builds would go out over http://.
Nothing else to set for the public address: with an Ingress enabled, publicUrls.appPublicUrl and publicUrls.publicBaseUrl are derived from ingress.host and ingress.tlsEnabled. That one value feeds nine settings in the core — the CORS origins and the GitHub, Google and Outlook Mail callbacks — so setting them by hand is how they end up disagreeing. Watch the certificate being issued:
Stuck at READY: False? See Kubernetes, Trap 3. Once READY: True with staging, swap the annotation to letsencrypt-prod, delete the staging secret so a real certificate is requested, and check the site in a browser:

Close port 80

The certificate does not close the clear-text entrance — Trap 7. With Traefik, apply the Middleware (Kubernetes, step 5), then add one annotation:
With ingress-nginx there is nothing to do: ssl-redirect applies as soon as a TLS section exists. Without an Ingress, nothing is exposed outside the cluster: since 0.4.0 the frontend Service is a ClusterIP — no provider load balancer, and no public IP billed for it. Set service.frontend.type=LoadBalancer, or reach it with kubectl port-forward. The install notes say which case you are in.

5. The first administrator

A fresh database has no administrator, and no route creates one. Two settings do:
An unknown account with a password is created as admin; an existing one is promoted, and its password is never rewritten. Without a password, an unknown account produces nothing — an account nobody can sign into helps nobody. So on an installation where people register themselves, set superadmin.email alone: the person signs up, and the next backend start promotes them. Then sign in and open Admin → Configuration: the screen lists every capability this installation has, and the sidebar badge counts what is still missing. That is the shortest path to knowing what is left to configure — see What needs configuring.

6. Upgrading and removing

Without --version this moves to the newest published chart; add --version <x.y.z> to choose one. Upgrading an existing install to 0.4.21: check where the database lives first. Since 0.4.21, PostgreSQL writes to /var/lib/postgresql/data/pgdata, a subdirectory of its volume, instead of the volume’s root: on ext4 volumes (OVH Cinder and most cloud block disks) the root holds a lost+found that made initdb fail. An install that already ran — so on a volume without lost+found (k3s local-path, kind, NFS) — keeps its database at the root. After the upgrade PostgreSQL would find pgdata/ empty and create a fresh database: the old data stays on the volume, but out of sight.
If PG_VERSION shows at the root, back up with pg_dumpall before upgrading, then restore into the new database. A fresh install is not affected. From 0.4.21 the backend also uses strategy: Recreate (its volume is ReadWriteOnce): each upgrade stops the old pod before starting the new one, so expect a few seconds of downtime. Uninstalling leaves the data volume behind, on purpose — it carries helm.sh/resource-policy: keep:

Storage

persistence.enabled claims one volume, mounts it on persistence.mountPath (/data) and points RUNTIME_ROOT at it. Everything the backend writes lives under it: bi_store, uploads, artifacts_store, agents_pool.
  • Without it, an imported CSV is gone at the next restart of the backend pod, and the dashboard reading it dies with it. agents_pool is the exception — it is regenerated from the database.
  • The claim is ReadWriteOnce, so the backend stays at one replica unless the storage class offers ReadWriteMany.
  • It carries helm.sh/resource-policy: keep: helm uninstall leaves the files behind rather than deleting them with the release.
Configuring S3 makes BI files bypass the disk entirely.

The seed is a Job

th2etl-seed posts the default pipelines and exits. It runs as a post-install,post-upgrade hook and retries while th2etl migrates the database. As a Deployment, Kubernetes would restart a container that exited 0 — a crash loop on a success.

A smaller stack

Nothing beyond the product itself is mandatory. The ./helm/apowerb-chart paths below are relative to a clone of apowerb-hosting:
Eight objects instead of sixteen. The interface then reports those features as not configured instead of failing on them — see What needs configuring.

From a checkout

Installing from a clone of the hosting repository takes the same five values. The chart refuses to install without four of them: postgres.password and backend.env.encryptKey, which have no default on purpose — a secret shipped in a public repository is not one — and the two th2pulse tokens. The backend and th2pulse do not start without them. Failing at install time is the honest moment to say it.
Keep those values. A later helm upgrade without them rewrites the Secret with empty strings — and ENCRYPT_KEY changing makes every connected integration unreadable. Put them in a values-secrets.yaml kept out of version control and pass it with --values.

Checks that need no cluster


Traps

Trap 1: th2etl and th2pulse restart twice

Symptom. Right after install, both show RESTARTS 2, with Connection refused on port 5432 in the previous container’s logs. Cause. They open the database before PostgreSQL accepts connections, exit, and are restarted. Up to chart 0.4.20, nothing waited on the database. Remedy. Chart 0.4.21 adds a wait-for-postgres init container to the backend, th2etl and th2pulse, which removes these restarts. On an older chart, none needed: Kubernetes recovers, and both reach Running within a couple of minutes. Worry only if the count keeps climbing — then read kubectl -n apowerb logs <pod> --previous, which will name a different cause.

Trap 2: an upgrade that empties the Secret

Symptom. After a helm upgrade, integrations no longer connect and agents lose their stored credentials. Cause. helm upgrade without --values values-secrets.yaml re-renders the Secret from the chart’s defaults — empty strings. ENCRYPT_KEY is what the integrations’ OAuth tokens were encrypted with; change it and they become unreadable. Remedy. Always pass the same values file. Back it up with your other secrets — it is not recoverable from the cluster once overwritten.

Trap 3: a Service stuck in Pending

Symptom. kubectl get svc shows EXTERNAL-IP: <pending> forever. Cause. service.frontend.type=LoadBalancer on a cluster whose provider gives no load balancer. Remedy. The chart defaults to ClusterIP for exactly this reason. Reach the interface through the Ingress, or kubectl port-forward.

Trap 4: pulling the chart from the old address

Symptom. helm install succeeds and the stack comes up — without th2etl, th2pulse, the collector, or the data volume. Cause. The chart used to be published to apowerb/apowerb, next to the backend images, and those old versions are still pullable. A stale command line therefore works, which is what makes it dangerous: it silently installs the stack as it was several versions ago. Remedy. Pull from apowerb/apowerb-chart, and check what you got:
The name must read apowerb-chart.

Trap 5: removing a secret from the values does not remove it from the cluster

Symptom. You delete a credential from values-secrets.yaml, run helm upgrade, and the Secret still holds it. Cause. The chart writes a credential only when it is non-empty ({{- if .Values.defaultLlm.apiKey }}). Dropping it from the values makes the template stop rendering the key — and Helm does not delete a key that is simply absent from the new render. Measured on 09/09/26: the shared-model API key was still in apowerb-secrets, 53 characters long, after an upgrade that no longer declared it. This is the mirror image of Trap 2, and the more dangerous of the two: there you lose a secret you meant to keep; here you keep a secret you meant to destroy. Remedy. Delete the key explicitly, then restart so the pods stop carrying it:
The Deployment keeps referencing the key with optional: true, so nothing breaks once it is gone — but check that before deleting a key from any chart that does not.

Trap 6: the model provider refuses the cluster’s outbound address

Symptom. The interface reports the shared model as configured, and every chat fails. The backend log names the provider, not the network:
Cause. The provider geo-blocks the address the cluster leaves from — which is the hosting provider’s NAT, not the region you picked. Measured on 09/09/26: the same key worked from one deployment and failed from a Kubernetes cluster at the same host, because the two products egress through different networks (AS214504 Hostman LTD versus AS210976 Timeweb), both geolocated in Germany. Diagnosis. Ask the pod, not your laptop — only its address matters:
Then look that address up: the autonomous system tells you more than the country. Test the providers themselves the same way — 401 means reachable and merely unauthenticated, a 403 or a 400 mentioning location means blocked.
Do not conclude “the key is invalid” from a 400 on Google’s API before forcing IPv4: the same key can answer 200 over IPv4 and 400 over IPv6, and the message is identical to the geo-block. Check whether the pod even has IPv6 (Network is unreachable settles it).
Remedy. Change provider — the fastest, and the checklist screen will tell users the truth meanwhile — or route the outbound traffic through an accepted address. Note that this affects every key: users’ own keys hit the same wall, not just the shared one.

Trap 7: HTTPS served, but HTTP still answers

Symptom. https:// works with a valid certificate, and http:// returns 200 with the same page instead of a redirect. Cause. ingress.tlsEnabled adds the TLS section; it does not close port 80. The controller keeps serving the site in clear text. Remedy. The Middleware above, or nginx’s ssl-redirect. Check with curl -sI http://<host>/ — you want 308, not 200. This matters more than it looks: a visitor typing the address without https:// submits their credentials unencrypted.

Trap 8: /health answers 200 in the cluster and 404 on the domain

Symptom. An external uptime monitor pointed at https://<host>/health reports the service down, while every pod is Running and the site loads in a browser. Cause. The Ingress carries a single rule — / to the frontend. The backend is not published; the frontend proxies /api/* to it server-side. So /api/config reaches the backend through the domain, while /health sits outside /api and is answered by the SPA, which returns 404 for a route it does not know. Remedy. Nothing to repair — /health is the kubelet’s liveness probe, and it answers where the probe runs. Verify it as in step 3, through a port-forward. For external monitoring, watch the site root (200) or /api/config, which is public and served by the backend: