- Published on
Self-Hosting Langfuse — Deployment Paths, Secrets, and What Catches You on First Boot
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- Opening — Know What You Are Starting
- The Fastest Path — docker compose
- The Values You Must Generate Yourself
- Connection Settings Per Datastore
- Object Storage Configuration
- The Kubernetes Path — the Helm Chart
- What to Check on First Boot
- Common Failures
- Before Moving to Production
- Closing — The Order Is the Diagnosis
- Try It Yourself
- Series
- References
Opening — Know What You Are Starting
The third post covered the components: the web and worker containers, plus Postgres, ClickHouse, Redis, and object storage. This post is about actually standing them up.
What eats the most time in self-hosting is not the deployment tool. It is one environment variable with the wrong value. And wrong values usually fail quietly: containers come up, login works, and only the traces never arrive.
Component and configuration names here were verified against the official documentation on 2026-08-15. Langfuse's architecture differs by version, so re-check the docs for the version you are running. The names and defaults below are from the v4 documentation. Values I could not confirm in the docs are not written here.
The Fastest Path — docker compose
The docker compose documentation walks you through cloning the repository and bringing compose up.
git clone https://github.com/langfuse/langfuse.git
cd langfuse
# change the secrets first, then bring it up
docker compose up -d
docker compose ps
Reading the compose file at the top of the repository on 2026-08-15, the defined services and images are as follows.
| Service | Image | Exposure |
|---|---|---|
| langfuse-web | docker.io/langfuse/langfuse:4 | 3000 published externally |
| langfuse-worker | docker.io/langfuse/langfuse-worker:4 | 3030 bound to loopback |
| postgres | docker.io/postgres (default tag 17) | loopback only |
| clickhouse | docker.io/clickhouse/clickhouse-server:25.12 | loopback only |
| redis | docker.io/redis:7 | loopback only |
| minio | cgr.dev/chainguard/minio | 9090 published externally |
Both Langfuse services declare health-check-based dependencies on all four datastores, so they do not start before the stores are ready. The documentation recommends putting a security group or firewall on the instance restricting incoming traffic to ports 3000 and 9090.
Lines in the compose file that need changing carry a # CHANGEME comment. Do not put this in production before every one of them is handled.
The Values You Must Generate Yourself
The configuration documentation marks four security-related variables as required.
| Variable | Purpose as documented |
|---|---|
NEXTAUTH_URL | URL of your Langfuse web deployment |
NEXTAUTH_SECRET | Used to validate login session cookies |
SALT | Used to salt hashed API keys |
ENCRYPTION_KEY | Used to encrypt sensitive data |
The documentation asks for 256 or more bits of entropy for NEXTAUTH_SECRET and SALT, and a 256-bit value in hex format for ENCRYPTION_KEY.
# Illustrative: generate values matching the required formats
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -base64 32 # SALT
openssl rand -hex 32 # ENCRYPTION_KEY (256-bit hex)
Change SALT or ENCRYPTION_KEY later and you lose the ability to read existing data. Decide them on day one and put them in a secrets manager. NEXTAUTH_URL has to match the address people actually reach, exactly. Behind a reverse proxy, leaving it as an internal address breaks the login redirect.
Connection Settings Per Datastore
On the Postgres side the documentation defines four variables.
DATABASE_URL— the Postgres connection string. Required.DIRECT_URL— the connection string used for database migrations. Defaults toDATABASE_URL; set it separately when you want a migration-specific user or a direct connection that bypasses a pooler.SHADOW_DATABASE_URL— required when the database user lacks CREATE DATABASE permission.LANGFUSE_AUTO_POSTGRES_MIGRATION_DISABLED— defaults to false; disables automatic migrations on startup.
On the ClickHouse side, having two connection strings is confusing the first time. The reason is that the protocols differ.
# Illustrative: two endpoints on different protocols
CLICKHOUSE_MIGRATION_URL="clickhouse://clickhouse-host:9000" # TCP, 9000 or 9440
CLICKHOUSE_URL="http://clickhouse-host:8123" # HTTP(S), 8123 or 8443
CLICKHOUSE_USER="langfuse"
CLICKHOUSE_PASSWORD="changeme"
CLICKHOUSE_DB="langfuse"
CLICKHOUSE_CLUSTER_ENABLED="false"
That is the pattern the ClickHouse documentation gives. Migrations go over the TCP protocol, ordinary queries over HTTP. CLICKHOUSE_DB defaults to default, and CLICKHOUSE_CLUSTER_ENABLED defaults to true — bring it down to false if you started a single container. The cluster name is set with CLICKHOUSE_CLUSTER_NAME, defaulting to default. If you need SSL, turn on CLICKHOUSE_MIGRATION_SSL.
User permissions are exactly as covered in the third post: INSERT, SELECT, ALTER UPDATE, ALTER DELETE, ALTER DROP INDEX, CREATE, and DROP TABLE. Miss any one of them and it stops at the migration step.
Redis is configured either with REDIS_CONNECTION_STRING alone or split into REDIS_HOST, REDIS_PORT, and REDIS_AUTH. For cluster and sentinel modes there are separate REDIS_CLUSTER_ENABLED and REDIS_SENTINEL_ENABLED families of variables. And do not forget the maxmemory-policy set to noeviction that the third post emphasized.
Object Storage Configuration
The object storage documentation marks the event upload bucket as required. The configuration page marks the media upload bucket as required too.
# Illustrative: using MinIO
LANGFUSE_S3_EVENT_UPLOAD_BUCKET="langfuse"
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT="http://minio:9000"
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID="minio"
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY="changeme"
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE="true"
LANGFUSE_S3_EVENT_UPLOAD_PREFIX="events/"
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET="langfuse"
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT="http://minio:9000"
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE="true"
Two things to remember. A prefix must end with a slash. And the documentation states that MinIO requires the force-path-style option; without it the bucket name is interpreted as a hostname and name resolution fails.
Batch export is off by default. Turning on LANGFUSE_S3_BATCH_EXPORT_ENABLED brings its own bucket configuration with it. On AWS S3 the minimum permissions are s3:PutObject, s3:ListBucket, and s3:GetObject on both the bucket and its objects.
The Kubernetes Path — the Helm Chart
The Helm documentation gives this chart repository and install command.
helm repo add langfuse https://langfuse.github.io/langfuse-k8s
helm repo update
helm install langfuse langfuse/langfuse -n langfuse --create-namespace
The default install brings up both application containers and the data stores. You can also point it at Postgres, ClickHouse, and Redis you already operate. The chart repository README describes the values structure as top-level keys langfuse, postgresql, clickhouse, redis, and s3 or minio.
# Illustrative: keeping secrets out of the values file
langfuse:
salt:
secretKeyRef:
name: langfuse-secrets
key: salt
nextauth:
secret:
secretKeyRef:
name: langfuse-secrets
key: nextauth-secret
encryptionKey:
secretKeyRef:
name: langfuse-secrets
key: encryption-key
postgresql:
auth:
username: langfuse
existingSecret: langfuse-postgres
clickhouse:
auth:
existingSecret: langfuse-clickhouse
redis:
auth:
existingSecret: langfuse-redis
s3:
storageProvider: s3
The exact nesting of these values differs by chart version, so confirm the real key names and defaults in the documentation for the version you are running and in the chart README. The README states two principles: passwords can be given directly or referenced through existingSecret and existingSecretKey, and using external data stores lets you decouple the datastore lifecycle from chart releases.
One thing to watch for. The documentation notes that if you install the chart under a release name other than langfuse, the Redis hostname has to be adjusted accordingly — a consequence of Helm's naming rules.
What to Check on First Boot
The Helm documentation says deployment can take up to five minutes and that the langfuse-web and langfuse-worker containers restart during database provisioning. In other words, the restarts you see early on are normal. If they keep repeating past five minutes, that is when it becomes a real problem.
# container state and logs
docker compose ps
docker compose logs -f langfuse-web
docker compose logs -f langfuse-worker
# check that web responds
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:3000
A good order to check things in:
- Confirm all four datastores are healthy. If you are stuck here, there is no point reading application logs.
- Confirm in the web logs that migrations finished — both Postgres and ClickHouse.
- Open the web UI, create an account, create a project. This far works with Postgres alone.
- Send one trace with the SDK. If it shows up, ClickHouse, Redis, and object storage are all alive.
Step four validates the entire ingestion path from the third post in one shot. That is why first-boot verification is not finished until you have done it.
Common Failures
Collecting the failure modes that have a basis in the documentation:
| Symptom | Cause | What to check |
|---|---|---|
| Queries return empty results | An infrastructure component is not on UTC | Timezone of every datastore container |
| Stops at the migration step | ClickHouse user lacks grants | The GRANT list from the third post |
| Migration fails on CREATE DATABASE | Postgres user lacks permission | SHADOW_DATABASE_URL |
| Queued events disappear | maxmemory-policy is not noeviction | Redis configuration |
| Bucket access fails | Path style option missing | LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE |
| Container dies on memory | Node heap ceiling not set | NODE_OPTIONS |
| Login redirect breaks | Mismatch with the external address | NEXTAUTH_URL |
The second-to-last item comes from the containers documentation. When the memory allocated to a container exceeds Node's default ceiling of roughly 1.7 GiB, not declaring the heap size through NODE_OPTIONS causes problems. It has to be set on both containers.
Log settings help when chasing startup problems. LANGFUSE_LOG_LEVEL defaults to info and ranges from trace to fatal. LANGFUSE_LOG_FORMAT defaults to text; switching it to json makes the logs easy to feed into a collection pipeline.
Before Moving to Production
The containers documentation recommends at least 2 CPUs and 4 GB of RAM for all containers. Run at least two web containers for availability, and add instances when CPU utilization exceeds 50 percent on either container.
Datastore recommendations are as summarized in the third post: ClickHouse with one shard and three or more replicas, Postgres at minimum 15 with 16 recommended for v4, and Redis 7.2 recommended.
Migration policy deserves a second look in production too. The default is automatic migration on startup. In environments where several instances come up at once, or when you want schema changes separated from deployment, turn on LANGFUSE_AUTO_POSTGRES_MIGRATION_DISABLED and LANGFUSE_AUTO_CLICKHOUSE_MIGRATION_DISABLED and run them as their own step. The ClickHouse documentation describes the manual procedure as cloning the repository, adjusting cluster names in the SQL under ./packages/shared/clickhouse/migrations/clustered/, and running them.
Closing — The Order Is the Diagnosis
Deploy datastores first and applications second and the blast radius of any problem stays small. Stand up the four stores and confirm healthy, generate the four secrets, fill in the connection strings, then bring up the application. Finally, send one trace to validate the whole path.
The check you can run today is on secrets management. If SALT and ENCRYPTION_KEY are sitting in plain text in a compose file or a values file, moving them into a secrets store comes before your next deployment. The next post looks at where costs grow once this system starts taking real traffic.
Try It Yourself
- HTTP Request Builder — build the first-boot health check request once and drop it straight into your deployment script.
- PostgreSQL Playground — practise splitting permissions between a migration account and an application account.
Series
- Previous: Why Langfuse Puts Traces in ClickHouse
- Next: When Traces Become Cost — Retention, Sampling, and Masking
References
- Langfuse docker compose deployment: https://langfuse.com/self-hosting/deployment/docker-compose
- Langfuse Kubernetes Helm deployment: https://langfuse.com/self-hosting/deployment/kubernetes-helm
- Langfuse configuration variables: https://langfuse.com/self-hosting/configuration
- Langfuse ClickHouse: https://langfuse.com/self-hosting/infrastructure/clickhouse
- Langfuse blob storage: https://langfuse.com/self-hosting/infrastructure/blobstorage
- Langfuse containers: https://langfuse.com/self-hosting/infrastructure/containers
- langfuse-k8s chart repository: https://github.com/langfuse/langfuse-k8s