Skip to content
API Blog

Kubernetes

Use the public Helm chart when you want to run Lobu on Kubernetes instead of a VM or local lobu run process. The chart deploys the API/chat gateway, connector worker, embeddings service, persistent volumes, optional ingress, and an optional migration hook.

  • A Kubernetes cluster with an ingress controller, if you want public webhooks or the admin UI.
  • Helm 3 with OCI registry support.
  • A reachable Postgres database with pgvector enabled.
  • Runtime secrets for Lobu (JWT_SECRET, BETTER_AUTH_SECRET, provider API keys, OAuth client secrets, and connector credentials as needed).

Create a namespace and store the database URL and runtime secrets in Kubernetes Secrets:

Terminal window
kubectl create namespace lobu
kubectl -n lobu create secret generic lobu-db \
--from-literal=uri='postgresql://USER:PASSWORD@HOST:5432/lobu?sslmode=require'
kubectl -n lobu create secret generic lobu-secrets \
--from-literal=JWT_SECRET='replace-with-a-long-random-value' \
--from-literal=BETTER_AUTH_SECRET='replace-with-a-long-random-value' \
--from-literal=ANTHROPIC_API_KEY='sk-ant-...'

Add any other provider, platform, or OAuth secrets your deployment needs to lobu-secrets.

Start a values.yaml with at least these fields:

secretName: lobu-secrets
database:
existingSecret: lobu-db
existingSecretKey: uri
# Run migrations as a Helm pre-install/pre-upgrade Job. SKIP_MIGRATIONS
# below stops app pods from also trying — the Job is the single writer.
migrations:
enabled: true
database:
existingSecret: lobu-db
existingSecretKey: uri
app:
env:
NODE_ENV: production
SKIP_MIGRATIONS: "1"
WORKER_ALLOWED_DOMAINS: ""
# Read by server.ts to mint OAuth redirect URIs, webhook callbacks,
# and invite links. MUST match your public origin.
PUBLIC_WEB_URL: "https://lobu.example.com"
ingress:
enabled: true
className: nginx
hosts:
- lobu.example.com

Make sure the secrets from the Create secrets section above already exist in the target namespace — values.yaml references them by name (lobu-db and lobu-secrets).

Always set app.env.PUBLIC_WEB_URL to your public origin (e.g. https://lobu.example.com). The server reads this to mint OAuth redirect URIs, webhook callbacks, and invite links; without it those URLs default to http://localhost:8787 and the install will be broken end-to-end.

Terminal window
helm install lobu oci://ghcr.io/lobu-ai/charts/lobu \
--namespace lobu --create-namespace \
-f values.yaml

The example values enable the Helm migration job and set SKIP_MIGRATIONS: "1" so app pods do not repeat migrations on startup.

Terminal window
kubectl -n lobu get pods -l app.kubernetes.io/instance=lobu
kubectl -n lobu logs deploy/lobu-app
kubectl -n lobu get ingress

If ingress is not enabled, port-forward the app service:

Terminal window
kubectl -n lobu port-forward svc/lobu-app 8787:8787
open http://localhost:8787

Check health through your public URL or the port-forward:

Terminal window
curl https://lobu.example.com/health
# or
curl http://localhost:8787/health

In production the app runs as a multi-replica Deployment. Two chart values must move together when you scale past one app pod:

app:
replicaCount: 3
service:
# Required when replicaCount > 1. Pins each client to one pod for the
# affinity window (default 3h). The chart's NOTES.txt prints a warning
# if you scale up without it.
sessionAffinity: ClientIP
sessionAffinityTimeoutSeconds: 10800

Affinity is mandatory because some per-pod state is in-memory and pod-local, with no cross-pod fan-out. Each pod owns its own SSE connections and their event backlog (SseManager), its own map of spawned worker subprocesses, and its deployment-creation lock cache. A client whose SSE stream (/api/v1/agents/<id>/events) lands on pod A but whose POST /messages lands on pod B would silently miss events. ClientIP affinity co-locates a client’s SSE stream, its message posts, and its conversation’s worker on one pod.

Cross-replica delivery rides Postgres, not memory. A worker reply reaches the client’s SSE pod through the thread_response queue: any pod’s consumer may claim a row and broadcast it to its own local SseManager. Platform responses (Slack, Telegram, and so on) are owner-routed, re-queued until the pod that owns the connection claims them.

The conversation workers the gateway spawns are child processes of their pod, not pods themselves. Scaling app.replicaCount scales the gateway and its local worker capacity together.

Schema migrations run once, from the Helm pre-install/pre-upgrade Job, never from app pods. That is why the example values set SKIP_MIGRATIONS: "1" on app.env. With N app pods, letting each pod run migrations on boot would race the schema. The Job is the single writer and the pods skip it.

Pin image tags for production upgrades, then run:

Terminal window
helm upgrade lobu oci://ghcr.io/lobu-ai/charts/lobu \
--namespace lobu \
-f values.yaml
  • Configure chat platforms from the admin UI or the connection API.
  • Review security before enabling broad worker network access.
  • Use observability for tracing, logs, and error monitoring.