Langfuse is an open-source observability platform for LLM applications including traces prompts/responses, tracks token usage and cost, and gives you debugging analytics for production AI workflows. This guide deploys it via Docker Compose with PostgreSQL, ClickHouse, Redis, and S3-compatible object storage, secured behind Traefik with Let's Encrypt, then sends a real trace through it.
Prerequisites: a Linux server (4 vCPU / 16GB RAM minimum), Docker + Docker Compose, an S3-compatible object storage bucket, a domain A record (e.g.
langfuse.example.com).
Set Up the Project
$ mkdir -p ~/langfuse/{letsencrypt,postgres,clickhouse-data,clickhouse-logs,redis}
$ cd ~/langfuse
ClickHouse runs as UID 101 in the container. Match ownership on its host dirs:
$ sudo chown -R 101:101 clickhouse-data clickhouse-logs
Generate 6 secrets (run 6 times, save each):
$ openssl rand -hex 32
First 3 → SALT, ENCRYPTION_KEY, NEXTAUTH_SECRET. Last 3 → POSTGRES_PASSWORD, CLICKHOUSE_PASSWORD, REDIS_AUTH.
Environment file:
$ nano .env
DOMAIN=langfuse.example.com
LETSENCRYPT_EMAIL=admin@example.com
NEXTAUTH_URL=https://langfuse.example.com
SALT=GENERATED_SECRET_1
ENCRYPTION_KEY=GENERATED_SECRET_2
NEXTAUTH_SECRET=GENERATED_SECRET_3
POSTGRES_USER=langfuse
POSTGRES_PASSWORD=GENERATED_SECRET_4
POSTGRES_DB=langfuse
DATABASE_URL=postgresql://langfuse:GENERATED_SECRET_4@postgres:5432/langfuse
CLICKHOUSE_USER=langfuse
CLICKHOUSE_PASSWORD=GENERATED_SECRET_5
CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000
CLICKHOUSE_URL=http://clickhouse:8123
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_AUTH=GENERATED_SECRET_6
S3_BUCKET=YOUR_BUCKET_NAME
S3_REGION=YOUR_REGION
S3_ENDPOINT=https://YOUR_S3_ENDPOINT
S3_ACCESS_KEY=YOUR_ACCESS_KEY
S3_SECRET_KEY=YOUR_SECRET_KEY
TELEMETRY_ENABLED=true
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES=false
TELEMETRY_ENABLED=true sends anonymous usage stats only (no prompts/traces) — set false if your compliance policy requires it. DATABASE_URL reuses POSTGRES_PASSWORD.
Deploy with Docker Compose
Six services: Traefik (TLS), PostgreSQL (metadata), ClickHouse (trace/metrics analytics), Redis (cache + queue), and two Langfuse components (web + worker).
$ nano docker-compose.yml
services:
traefik:
image: traefik:v3.7.0
container_name: traefik
restart: unless-stopped
environment:
DOCKER_API_VERSION: "1.44"
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.le.acme.httpchallenge=true"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
postgres:
image: postgres:17
container_name: langfuse-postgres
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
TZ: UTC
PGTZ: UTC
ports:
- "127.0.0.1:5432:5432"
volumes:
- ./postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 3s
timeout: 3s
retries: 10
clickhouse:
image: clickhouse/clickhouse-server:26.5.1-alpine
container_name: langfuse-clickhouse
restart: unless-stopped
user: "101:101"
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
ports:
- "127.0.0.1:8123:8123"
- "127.0.0.1:9000:9000"
volumes:
- ./clickhouse-data:/var/lib/clickhouse
- ./clickhouse-logs:/var/log/clickhouse-server
healthcheck:
test: wget --no-verbose --tries=1 --spider http://127.0.0.1:8123/ping || exit 1
interval: 5s
timeout: 5s
retries: 10
start_period: 1s
redis:
image: redis:7-alpine
container_name: langfuse-redis
restart: unless-stopped
command: >
--requirepass ${REDIS_AUTH}
--maxmemory-policy noeviction
ports:
- "127.0.0.1:6379:6379"
volumes:
- ./redis:/data
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 3s
timeout: 10s
retries: 10
langfuse-worker:
image: langfuse/langfuse-worker:3
container_name: langfuse-worker
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "127.0.0.1:3030:3030"
environment:
DATABASE_URL: ${DATABASE_URL}
NEXTAUTH_URL: ${NEXTAUTH_URL}
SALT: ${SALT}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL}
CLICKHOUSE_URL: ${CLICKHOUSE_URL}
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_CLUSTER_ENABLED: "false"
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
REDIS_AUTH: ${REDIS_AUTH}
REDIS_TLS_ENABLED: "false"
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${S3_BUCKET}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${S3_REGION}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: "events/"
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${S3_BUCKET}
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${S3_REGION}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: "media/"
LANGFUSE_S3_BATCH_EXPORT_ENABLED: "true"
LANGFUSE_S3_BATCH_EXPORT_BUCKET: ${S3_BUCKET}
LANGFUSE_S3_BATCH_EXPORT_REGION: ${S3_REGION}
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_BATCH_EXPORT_PREFIX: "exports/"
langfuse-web:
image: langfuse/langfuse:3
container_name: langfuse-web
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- "3000:3000"
environment:
DATABASE_URL: ${DATABASE_URL}
NEXTAUTH_URL: ${NEXTAUTH_URL}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
SALT: ${SALT}
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED}
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: ${LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES}
CLICKHOUSE_MIGRATION_URL: ${CLICKHOUSE_MIGRATION_URL}
CLICKHOUSE_URL: ${CLICKHOUSE_URL}
CLICKHOUSE_USER: ${CLICKHOUSE_USER}
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
CLICKHOUSE_CLUSTER_ENABLED: "false"
REDIS_HOST: ${REDIS_HOST}
REDIS_PORT: ${REDIS_PORT}
REDIS_AUTH: ${REDIS_AUTH}
REDIS_TLS_ENABLED: "false"
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: ${S3_BUCKET}
LANGFUSE_S3_EVENT_UPLOAD_REGION: ${S3_REGION}
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: "events/"
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: ${S3_BUCKET}
LANGFUSE_S3_MEDIA_UPLOAD_REGION: ${S3_REGION}
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: "media/"
LANGFUSE_S3_BATCH_EXPORT_ENABLED: "true"
LANGFUSE_S3_BATCH_EXPORT_BUCKET: ${S3_BUCKET}
LANGFUSE_S3_BATCH_EXPORT_REGION: ${S3_REGION}
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: ${S3_ACCESS_KEY}
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY: ${S3_SECRET_KEY}
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: ${S3_ENDPOINT}
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_BATCH_EXPORT_PREFIX: "exports/"
labels:
- "traefik.enable=true"
- "traefik.http.routers.langfuse.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.langfuse.entrypoints=websecure"
- "traefik.http.routers.langfuse.tls=true"
- "traefik.http.routers.langfuse.tls.certresolver=le"
- "traefik.http.services.langfuse.loadbalancer.server.port=3000"
langfuse-worker handles async trace processing and batch exports to object storage; langfuse-web is the UI/API. Both wait on postgres/redis/clickhouse health.
$ docker compose up -d
$ docker compose ps -a
$ docker compose logs
postgres, clickhouse, redis should show healthy (the only ones with configured checks).
First-Run Setup
Visit https://langfuse.example.com:
- Sign up — email + password.
- Create an organization (e.g.
My Company) → Create Organization. - Create a project (e.g.
Production LLM) → Create Project. -
Create API keys in the setup wizard — copy both Secret Key (
sk-lf-...) and Public Key (pk-lf-...). The secret is shown once only.
Send a Test Trace
$ sudo apt install python3-venv -y
$ python3 -m venv langfuse-env
$ source langfuse-env/bin/activate
$ pip install langfuse openai
$ nano test_langfuse.py
from langfuse.openai import openai
client = openai.OpenAI(
api_key="YOUR_LLM_API_KEY",
base_url="https://api.groq.com/openai/v1"
)
response = client.chat.completions.create(
model="openai/gpt-oss-120b",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain Langfuse in one sentence."}
],
temperature=0.7,
max_tokens=100
)
print(response.choices[0].message.content)
Swap base_url for your actual LLM provider if not using Groq.
$ export LANGFUSE_SECRET_KEY="YOUR_SECRET_KEY"
$ export LANGFUSE_PUBLIC_KEY="YOUR_PUBLIC_KEY"
$ export LANGFUSE_HOST="https://langfuse.example.com"
$ python test_langfuse.py
The Langfuse SDK auto-captures the request/response and forwards it as a trace.
In the UI, go to your project's Traces page — click the trace to see model name, token usage, latency, cost estimate, and the full conversation.
Next Steps
Langfuse is running with full trace persistence, analytics via ClickHouse, and object-storage-backed exports. From here:
- Wire Langfuse into your actual production LLM app via its SDK
- Set up cost/latency alerting on traces that exceed thresholds
- Use batch exports for offline analysis of historical trace data
For the full guide, visit the original article on Vultr Docs.
Top comments (0)