Deploying means installing the Helm chart,
apowerb-chart. The static
manifests in the repository’s k8s/ folder predate it and describe an
incomplete stack — see Trap 8.What you need before starting
1. Reach the cluster
Ready. kubectl version should show a client at most one
minor version away from the server; further apart, some commands fail in
puzzling ways.
2. Storage
Check first — this is the single most common reason nothing starts.No resources found means no volume can ever be created: PostgreSQL and the
data volume would stay Pending forever, and the pods with them. Managed
clusters differ wildly here; some ship a class, some only offer object storage
(which does not suit PostgreSQL).
If the list is empty, local-path-provisioner gives you node-local volumes in
one manifest:
local-path (default).
Prove it works before going further — and mind that the check needs a pod,
not just a claim (Trap 1):
Bound, and ok in the logs. Anything else, stop here: the rest cannot work.
3. Ingress controller
Needed only for a public address; skip it if you will useport-forward.
Install Traefik or ingress-nginx — most managed Kubernetes offerings
have both as a one-click add-on, which is the simplest path since the
provider then also wires the load balancer. Then:
traefik, nginx…) and the EXTERNAL-IP of the
controller’s service: that address is what your DNS record will point to.
4. cert-manager and an issuer
Installing cert-manager is not enough — it issues nothing until an issuer exists.https://acme-v02.api.letsencrypt.org/directory and its own key secret name.
Both are ready-made in the hosting repository under
k8s/cert-manager/cluster-issuer-letsencrypt.yaml.
READY: True on both means cert-manager registered with the ACME servers.
5. Secrets
Five values. Two of them are mandatory — the chart refuses to install without the th2pulse tokens, because that service does not start without them.6. Install
Start without an Ingress: fewer moving parts, and a public name is not needed to know whether the stack runs.7. Check what actually runs
- six pods
Running: backend, frontend, postgres, th2etl, th2pulse, otel-collector; apowerb-th2etl-seedCompleted— it is aJob, so0/1 Completedis the success state, not a failure;- two PVCs
Bound: the data volume and the database’s.
th2etl and th2pulse at first start are expected —
Trap 3.
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 12).
8. Going public
Point an A record at theEXTERNAL-IP from step 3, wait for it to
resolve, then upgrade with the Ingress — with the staging issuer first:
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:
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 9. With Traefik, a Middleware and one more annotation:ssl-redirect applies as soon as a
TLS section exists.
9. The first administrator
A fresh database has no administrator, and no route creates one. Two settings do: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.
10. Upgrading and removing
helm.sh/resource-policy: keep:
Traps
Trap 1: a PVC that stays Pending on purpose
Symptom. You create a claim to test the storage; it staysPending. It
looks like the storage is broken.
Cause. Storage classes with volumeBindingMode: WaitForFirstConsumer —
local-path among them — only create the volume when a pod needs it. This
is by design: the volume is placed on the node the pod is scheduled to.
Remedy. Test with a claim and a pod, as in step 2.
kubectl get storageclass shows the mode.
Trap 2: no storage class at all
Symptom. SamePending, but with a pod waiting too, and
kubectl describe pvc says: no persistent volumes available for this claim and no storage class is set.
Cause. The cluster has no default storage class — common on lean managed
clusters.
Remedy. Step 2. Check kubectl get storageclass before
installing, not after wondering why nothing starts.
Trap 3: th2etl and th2pulse restart twice
Symptom. Right after install, both showRESTARTS 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. The chart has no init container waiting on the database.
Remedy. 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 4: an upgrade that empties the Secret
Symptom. After ahelm 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 5: a certificate stuck at Ready: False
Symptom.kubectl get certificate shows READY: False and stays there,
without an obvious error.
Causes, in the order worth checking.
- The DNS name does not resolve to the ingress load balancer yet, or port 80 is closed — the HTTP-01 challenge needs both.
- The issuer uses
classinstead ofingressClassName(see step 4). - The Let’s Encrypt rate limit has been hit — five failures in an hour blocks the account for a week. This is what the staging issuer is for.
Challenge object names the real reason.
Trap 6: 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 7: 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:
name must read apowerb-chart.
Trap 11: removing a secret from the values does not remove it from the cluster
Symptom. You delete a credential fromvalues-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 4, 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:
optional: true, so nothing breaks once it is
gone — but check that before deleting a key from any chart that does not.
Trap 10: 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:AS214504 Hostman LTD versus AS210976 Timeweb), both
geolocated in Germany.
Diagnosis. Ask the pod, not your laptop — only its address matters:
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).Trap 9: 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 12: /health answers 200 in the cluster and 404 on the domain
Symptom. An external uptime monitor pointed athttps://<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 7, through a
port-forward. For external monitoring, watch the site root (200) or
/api/config, which is public and served by the backend:
Trap 8: the old static manifests
Symptom.kubectl apply -f k8s/ installs something that looks right but
has no orchestrator, no log store, no collector and no data volume, with an
Ingress on apowerb.local.
Cause. Those manifests predate the chart and were kept for reference.
Remedy. Use the chart. The k8s/ folder now holds only what lives
beside a release — the cert-manager issuers, which are cluster-wide and do
not belong to an application chart.