A Nebari software pack that deploys Langfuse - an open-source LLM observability, tracing, evaluation, and prompt-management platform - on a Nebari cluster with full Keycloak SSO integration.
This pack wraps the upstream langfuse/langfuse Helm chart 1.5.34 (Langfuse app version
3.179.1). Declared maturity level: Alpha (level: alpha in pack-metadata.yaml).
- A running Nebari cluster managed by nebari-operator v0.1.0-alpha.19 or later
- cert-manager installed and able to issue certificates for your domain
- Envoy Gateway installed (the NebariApp CRD uses it for routing)
- Keycloak installed and accessible (the operator provisions an OIDC client)
- The target namespace labeled
nebari.dev/managed=true:kubectl label namespace <namespace> nebari.dev/managed=true
- Helm 3
Only a Kubernetes cluster and Helm 3 are required. No Keycloak or Envoy Gateway needed.
Add the Nebari Helm repository and install the pack. The upstream langfuse/langfuse chart is
a transitive dependency - you only need to add the Nebari repo.
helm repo add nebari https://nebari-dev.github.io/helm-repository
helm repo update
helm install langfuse nebari/nebari-langfuse \
-f examples/nebari-values.yaml \
--set nebariapp.hostname=langfuse.<your-domain> \
--set langfuse.langfuse.nextauth.url=https://langfuse.<your-domain> \
--set langfuse.langfuse.auth.providers.keycloak.issuer=https://keycloak.<your-domain>/realms/nebari \
--namespace <namespace>Using release name langfuse is recommended. The pack pins
langfuse.fullnameOverride: langfuse so the web Service is always named langfuse-web.
See examples/nebari-values.yaml for a minimal values file to use with -f.
The upstream chart dependency is named langfuse, and the upstream chart also uses a
top-level langfuse key for app configuration. This creates double-nesting:
| What you are setting | Path from this wrapper |
|---|---|
| App config (auth, nextauth, secrets, ...) | langfuse.langfuse.* |
| PostgreSQL sub-chart | langfuse.postgresql.* |
| Redis sub-chart | langfuse.redis.* |
| ClickHouse sub-chart | langfuse.clickhouse.* |
| S3/MinIO sub-chart | langfuse.s3.* |
| Chart-level (fullnameOverride) | langfuse.fullnameOverride |
Example:
langfuse:
langfuse: # double-nested: app config
nextauth:
url: https://langfuse.example.com
auth:
providers:
keycloak:
issuer: https://keycloak.example.com/realms/nebari
postgresql: # single-nested: sub-chart
deploy: trueSee the configuration reference
(source: docs/src/content/docs/configuration.md) for the full values reference.
Langfuse uses app-native OAuth (NextAuth) with Keycloak as the provider.
How it works:
-
Set
nebariapp.auth.enabled: truein your values file. The pack setsnebariapp.auth.provisionClient: trueby default, which instructs the nebari-operator to create a Keycloak client automatically. -
The operator registers the redirect URI
https://<hostname>/api/auth/callback/keycloak(and thehttp://variant) on the provisioned Keycloak client. No manual Keycloak step is needed whenprovisionClient: true. -
You must set
langfuse.langfuse.auth.providers.keycloak.issuerto your Keycloak realm URL. The operator does not reliably emit the issuer URL into the OIDC secret, so this value cannot be read automatically:--set langfuse.langfuse.auth.providers.keycloak.issuer=https://keycloak.<your-domain>/realms/nebari
Or in your values file:
langfuse: langfuse: auth: providers: keycloak: issuer: "https://keycloak.<your-domain>/realms/nebari"
-
The
clientIdandclientSecretare read automatically from thelangfuse-oidc-clientsecret that the operator creates.
Note on enforceAtGateway: This is set to false by default. Langfuse handles OAuth
itself (app-native). The Envoy SecurityPolicy is not applied to the route.
If you use provisionClient: false with a manually created Keycloak client, you must
add https://<hostname>/api/auth/callback/keycloak to that client's Valid Redirect URIs
yourself.
On first install, the pack generates a Kubernetes Secret named langfuse-secrets containing:
| Key | Description |
|---|---|
salt |
Salt used to hash project API keys |
encryptionKey |
64-character hex key for encrypting integration credentials |
nextauth-secret |
NextAuth.js session signing key |
postgres-password |
PostgreSQL password |
redis-password |
Redis password |
clickhouse-password |
ClickHouse password |
root-user |
MinIO/S3 root username |
root-password |
MinIO/S3 root password |
The secret is persisted using helm.sh/resource-policy: keep and Helm lookup so it survives
helm upgrade without rotation.
WARNING: Rotating encryptionKey orphans any integration credentials already encrypted
with the old key. Do not rotate it unless you are prepared to re-enter all integrations.
ArgoCD's helm template rendering cannot perform cluster API lookups, so the auto-generation
path (secrets.generate: true) does not work in a pure GitOps setup. For production or GitOps:
- Set
secrets.generate: falsein your values. - Create the
langfuse-secretsSecret manually before the first sync:kubectl create secret generic langfuse-secrets \ --from-literal=salt=$(openssl rand -hex 16) \ --from-literal=encryptionKey=$(openssl rand -hex 32) \ --from-literal=nextauth-secret=$(openssl rand -hex 32) \ --from-literal=postgres-password=$(openssl rand -hex 16) \ --from-literal=redis-password=$(openssl rand -hex 16) \ --from-literal=clickhouse-password=$(openssl rand -hex 16) \ --from-literal=root-user=langfuse \ --from-literal=root-password=$(openssl rand -hex 16) \ -n <namespace>
- If needed, add an ArgoCD
ignoreDifferencesentry for the Secret resource so ArgoCD does not flag the pre-created secret as out-of-sync.
By default the pack deploys bundled bitnamilegacy/* images for PostgreSQL, Redis,
ClickHouse, and MinIO. These are suitable for development and demos.
For production, use external managed services. See examples/prod-external-datastores.yaml
for a complete example. Disable each bundled datastore with deploy: false:
langfuse:
postgresql:
deploy: false
host: <managed-postgres-host>
port: 5432
auth:
username: langfuse
database: langfuse
existingSecret: <your-secret>
redis:
deploy: false
host: <managed-redis-host>
clickhouse:
deploy: false
host: <managed-clickhouse-host>
s3:
deploy: false
bucket: <your-bucket>To run Langfuse without Nebari integration (email/password auth, bundled datastores):
helm repo add nebari https://nebari-dev.github.io/helm-repository
helm repo update
helm install langfuse nebari/nebari-langfuse \
-f examples/standalone-values.yamlOnce deployed, forward the web port:
kubectl port-forward svc/langfuse-web 3000:3000Then open http://localhost:3000 and sign up with email/password.
-
Frozen bitnamilegacy images: The bundled PostgreSQL, Redis, ClickHouse, and MinIO images are frozen
bitnamilegacy/*variants pinned by upstream chart 1.5.34. They do not receive ongoing security patches. Use external managed datastores in production. -
Generated secrets incompatible with ArgoCD GitOps: The
secrets.generate: truepath uses a Helm cluster lookup that ArgoCD'shelm templaterenderer cannot execute. For GitOps or production deployments, setsecrets.generate: falseand pre-create thelangfuse-secretsSecret as described above. -
Must set AUTH_KEYCLOAK_ISSUER manually: The nebari-operator does not reliably emit the Keycloak issuer URL into the OIDC secret. You must set
langfuse.langfuse.auth.providers.keycloak.issuerto your realm URL explicitly. -
ClickHouse runs single-node by default: This pack defaults to a single, non-clustered ClickHouse (
langfuse.clickhouse.clusterEnabled: false,replicaCount: 1,zookeeper.enabled: false). The upstream chart defaults to a 3-replica cluster, but Langfuse runs its schema migrations over the load-balanced ClickHouse Service and theschema_migrationsbookkeeping is not consistently replicated across replicas, so migrations get marked "dirty" and crashlooplangfuse-web. Single-node avoids this and is far lighter. For HA, setclusterEnabled: true,replicaCount: 3,zookeeper.enabled: true(and accept the migration caveat), or use an external managed ClickHouse. -
No native Prometheus metrics endpoint: Langfuse does not expose a
/metricsendpoint by default. Themetrics.podMonitor.enabledopt-in (defaultfalse) is a hook for a future sidecar exporter. Enabling it without a metrics endpoint produces scrape errors.
Check the NebariApp status and events:
kubectl describe nebariapp langfuse -n <namespace>Common conditions and fixes:
-
NamespaceNotOptedIn: The namespace is missing the required label.kubectl label namespace <namespace> nebari.dev/managed=true
-
ServiceNotFound: Thelangfuse-webService is missing. Check that the Helm release deployed successfully and thatlangfuse.fullnameOverride: langfuseis set (it is the default in this pack):kubectl get svc -n <namespace> helm status langfuse -n <namespace>
The bundled datastores use bitnamilegacy/* images from Docker Hub. If the cluster has
restricted egress or Docker Hub rate limits apply, pulls may fail:
kubectl get events -n <namespace> --field-selector reason=Failed
kubectl describe pod <pod-name> -n <namespace>For restricted environments, mirror the images to an internal registry and override the image repository in your values file, or switch to external datastores.
This only applies when nebariapp.auth.provisionClient: false. If you created the Keycloak
client manually, ensure its Valid Redirect URIs include:
https://<hostname>/api/auth/callback/keycloak
When provisionClient: true (the default), the operator registers this URI automatically.
If the langfuse-web pod crashloops with an error about ENCRYPTION_KEY being the wrong
length, the langfuse-secrets Secret is missing or has a malformed key. Verify:
kubectl get secret langfuse-secrets -n <namespace> -o jsonpath='{.data.encryptionKey}' | base64 -d | tr -d '\n' | wc -cThe decoded value must be exactly 64 hex characters (wc -c should print 64). If missing,
check secrets.generate and recreate the secret if needed.
ClickHouse requests significant CPU and memory. On small clusters (fewer than 3 schedulable nodes), pods may stay Pending:
kubectl get pods -n <namespace> -l app.kubernetes.io/name=clickhouse
kubectl describe pod <clickhouse-pod> -n <namespace>Reduce to a single replica for development:
langfuse:
clickhouse:
replicaCount: 1Or disable the bundled ClickHouse and point to an external instance.
Metrics: Langfuse does not expose a native Prometheus /metrics endpoint. The pack
includes an opt-in metrics.podMonitor.enabled flag (default false) as a hook for a future
sidecar exporter. Enabling it requires Prometheus Operator CRDs to be installed. Do not enable
it without a corresponding metrics exporter.
Logs: Langfuse writes structured JSON logs to stdout/stderr on the web and worker containers. These are captured by any standard cluster log shipper (Fluentd, Vector, etc.) targeting pod stdout.
For sending traces from the Nebari OTel Collector to Langfuse, see configuration reference.
- Configuration reference
(
docs/src/content/docs/configuration.md) - Full values reference, auth wiring, secrets, external datastores, telemetry, and OTel Collector integration. tests/e2e/README.md- End-to-end test suite for this pack.examples/nebari-values.yaml- Minimal Nebari deployment values.examples/standalone-values.yaml- Standalone / local deployment values.examples/prod-external-datastores.yaml- Production values with external managed datastores.examples/argocd-app.yaml- ArgoCD Application manifest.
The docs site lives in docs/ and is built with Astro +
Starlight using the shared @nebari/starlight theme. It
deploys to packs.nebari.dev/langfuse-pack/ on
every merge to main; pull requests that touch docs/ get a preview URL posted as a
comment.
cd docs
npm ci
npm run dev # dev server with hot reload at http://localhost:4321
npm run build # static build into docs/dist/
npm test # unit testsPages live in docs/src/content/docs/ - each .md or .mdx file becomes a page, and the
sidebar is configured in docs/astro.config.mjs. See docs/README.md for
details.