Self-hosting Langfuse gives you full control over LLM trace data, and this guide walks through standing up the stack on a single Linux box before scaling out. We’ll cover the Docker Compose approach, environment configuration, application instrumentation, and how to verify that telemetry is actually landing in your instance.
Step 1: Provision a host and install dependencies
Use a machine with at least 4 vCPU, 8 GB RAM, and 50 GB of SSD-backed disk for a modest load. Langfuse writes traces to ClickHouse and relational metadata to Postgres, so I/O latency directly affects ingest throughput. A small cloud VM or bare-metal node works.
Install Docker and the compose plugin:
# Ubuntu 22.04
sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose-plugin postgresql-client
sudo systemctl enable --now docker
Add your user to the docker group so you don’t prefix every command with sudo:
sudo usermod -aG docker $USER
newgrp docker
Confirm the install:
docker version
docker compose version
Step 2: Fetch the Langfuse Compose stack
Pin to a specific release tag to avoid surprise breakages. The project ships a reference docker-compose.yml in its GitHub repository. When self-hosting Langfuse, locking the version is non-negotiable for reproducible rollouts.
mkdir -p ~/langfuse && cd ~/langfuse
curl -L https://raw.githubusercontent.com/langfuse/langfuse/v2.0.0/docker-compose.yml -o docker-compose.yml
Inspect the file. You will see services: langfuse-server, langfuse-worker, langfuse-web, postgres, clickhouse, redis, and optionally minio for artifact storage. For a single-node deployment the defaults are sane. If you already run Postgres or ClickHouse elsewhere, point the env vars at those instead and remove the local service.
Step 3: Configure environment
Create a .env file in the same directory. The minimal required variables:
cat > .env <<'EOF'
# Database connections (internal docker network)
DATABASE_URL=postgresql://langfuse:langfuse@postgres:5432/langfuse
CLICKHOUSE_URL=http://clickhouse:8123
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=
REDIS_HOST=redis
REDIS_PORT=6379
# Bootstrap auth (generate below)
LANGFUSE_SECRET_KEY=sk-lf-00000000000000000000000000000000
LANGFUSE_PUBLIC_KEY=pk-lf-00000000000000000000000000000000
# Session encryption
NEXTAUTH_SECRET=replace-with-openssl-rand-hex-32
NEXTAUTH_URL=http://localhost:3000
# Project ID hashing salt
SALT=make-up-a-long-random-string
EOF
Generate real values:
openssl rand -hex 32 # for LANGFUSE_SECRET_KEY (append after sk-lf-)
openssl rand -hex 32 # NEXTAUTH_SECRET
openssl rand -hex 16 # SALT
Note the key format: LANGFUSE_SECRET_KEY must start with sk-lf- followed by 32 hex characters; LANGFUSE_PUBLIC_KEY starts with pk-lf-. The server rejects malformed keys on boot.
Optional but recommended for production: configure SMTP for invite emails and S3 for trace attachments.
SMTP_CONNECTION_URL=smtps://user:pass@smtp.example.com:465
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=my-langfuse-bucket
LANGFUSE_S3_EVENT_UPLOAD_REGION=us-east-1
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
Step 4: Start the stack
Bring it up detached:
docker compose up -d
On first boot the server container runs database migrations automatically. Watch the logs:
docker compose logs -f langfuse-server
You should see lines similar to Migrations applied and Server listening on :3000. The worker will show Connected to Redis and start consuming the queue. When self-hosting Langfuse, always confirm both server and worker are healthy before sending traffic.
Step 5: Create a project and API keys
Open http://<host-ip>:3000 in a browser. Register the first user (this becomes the admin). Navigate to Settings → Projects → Create, name it prod-app. The UI returns a project ID, a public key (pk-lf-…), and a secret key (sk-lf-…).
These project keys are what your application SDK uses. The .env keys from Step 3 are only for bootstrapping the instance. Store the project keys in your secret manager.
Step 6: Instrument your application
Install the SDK:
pip install langfuse openai
A minimal traced call:
from langfuse import Langfuse
from openai import OpenAI
lf = Langfuse(
public_key="pk-lf-...",
secret_key="sk-lf-...",
host="http://<host-ip>:3000"
)
client = OpenAI() # or any OpenAI-compatible endpoint
def chat(trace_id: str, user_input: str):
with lf.start_as_current_span(name="chat", trace_id=trace_id) as span:
span.update_metadata({"input": user_input})
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": user_input}]
)
span.update_metadata({"output": resp.choices[0].message.content})
lf.flush()
return resp
If you route inferences through n4n.ai, its OpenAI-compatible endpoint drops into the same OpenAI() client with base_url="https://api.n4n.ai/v1" and forwards provider cache-control hints, so the Langfuse span captures the exact tokens billed.
client = OpenAI(base_url="https://api.n4n.ai/v1", api_key="your-n4n-key")
For TypeScript applications:
import { Langfuse } from "langfuse";
const langfuse = new Langfuse({
publicKey: "pk-lf-...",
secretKey: "sk-lf-...",
baseUrl: "http://<host-ip>:3000"
});
The SDK buffers events and flushes on exit or when flush() is called. In serverless environments, explicitly flush before the handler returns.
Step 7: Verify traces are flowing
Two verification paths: UI and direct ClickHouse query.
In the Langfuse UI, open Tracing. After running chat() once, you should see a new trace named chat within a few seconds. Expand it to confirm input/output metadata and token counts.
For a definitive, scriptable check, query ClickHouse:
docker exec -it langfuse-clickhouse clickhouse-client
SELECT count() FROM traces WHERE created_at > now() - INTERVAL 5 MINUTE;
A non-zero result proves the full ingest pipeline works. If the count is zero, enable SDK debug logging:
import os
os.environ["LANGFUSE_DEBUG"] = "true"
Common culprits: forgotten lf.flush(), wrong host URL, or network egress blocked from app to port 3000.
Success criteria: at least one trace visible in UI AND count() > 0 in ClickHouse.
Step 8: Persist data and back up
The compose file declares named volumes postgres_data, clickhouse_data, and redis_data. Snapshot them with your existing backup tooling.
Postgres logical dump:
docker exec langfuse-postgres pg_dump -U langfuse langfuse > langfuse-$(date +%F).sql
ClickHouse is append-only; use clickhouse-backup or a volume snapshot. Redis is only a job queue, so losing it is non-fatal (in-flight jobs restart).
Add a nightly cron for Postgres:
0 2 * * * docker exec langfuse-postgres pg_dump -U langfuse langfuse | gzip > /backups/langfuse-$(date +\%F).sql.gz
Set a retention policy on traces to control disk growth:
docker exec langfuse-clickhouse clickhouse-client --query "ALTER TABLE traces DELETE WHERE created_at < now() - INTERVAL 30 DAY"
Step 9: Scale and harden
For higher load, run multiple langfuse-worker replicas behind the same Redis. They are stateless and consume the queue concurrently.
Terminate TLS at a reverse proxy:
server {
listen 443 ssl;
server_name langfuse.internal;
ssl_certificate /etc/ssl/langfuse.crt;
ssl_certificate_key /etc/ssl/langfuse.key;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Update NEXTAUTH_URL to https://langfuse.internal and restart. When self-hosting Langfuse, treat every upgrade as a database migration event: pull the new tag, run docker compose up -d, and watch migration logs. Never skip minor versions without reading release notes.
Troubleshooting
- ClickHouse connection refused: ensure
CLICKHOUSE_URLuseshttp://, notclickhouse://. - Empty traces in UI: verify
lf.flush()is called; the SDK buffers up to 1 MB or 10 s by default. - Worker OOM: raise
REDIS_MAX_MEMORYor scale workers horizontally. - High disk usage: schedule partition drops in ClickHouse as shown above.
Self-hosting Langfuse is straightforward with Compose, but the operational weight sits on Postgres and ClickHouse backups. Get those right and you have a sovereign observability layer for every LLM call your stack makes.