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.
Prerequisites
Section titled “Prerequisites”- 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 secrets
Section titled “Create secrets”Create a namespace and store the database URL and runtime secrets in Kubernetes Secrets:
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.
Prepare values
Section titled “Prepare values”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.comMake 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.
Install
Section titled “Install”helm install lobu oci://ghcr.io/lobu-ai/charts/lobu \ --namespace lobu --create-namespace \ -f values.yamlThe example values enable the Helm migration job and set SKIP_MIGRATIONS: "1" so app pods do not repeat migrations on startup.
Verify
Section titled “Verify”kubectl -n lobu get pods -l app.kubernetes.io/instance=lobukubectl -n lobu logs deploy/lobu-appkubectl -n lobu get ingressIf ingress is not enabled, port-forward the app service:
kubectl -n lobu port-forward svc/lobu-app 8787:8787open http://localhost:8787Check health through your public URL or the port-forward:
curl https://lobu.example.com/health# orcurl http://localhost:8787/healthScaling and multi-replica operation
Section titled “Scaling and multi-replica operation”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: 10800Affinity 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.
Migrations stay single-writer
Section titled “Migrations stay single-writer”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.
Upgrade
Section titled “Upgrade”Pin image tags for production upgrades, then run:
helm upgrade lobu oci://ghcr.io/lobu-ai/charts/lobu \ --namespace lobu \ -f values.yamlNext steps
Section titled “Next steps”- 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.