DEV Community

Cover image for Deploying Langfuse – Open-Source LLM Observability Platform
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya • Originally published at docs.vultr.com

Deploying Langfuse – Open-Source LLM Observability Platform

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
Enter fullscreen mode Exit fullscreen mode

ClickHouse runs as UID 101 in the container. Match ownership on its host dirs:

$ sudo chown -R 101:101 clickhouse-data clickhouse-logs
Enter fullscreen mode Exit fullscreen mode

Generate 6 secrets (run 6 times, save each):

$ openssl rand -hex 32
Enter fullscreen mode Exit fullscreen mode

First 3 → SALT, ENCRYPTION_KEY, NEXTAUTH_SECRET. Last 3 → POSTGRES_PASSWORD, CLICKHOUSE_PASSWORD, REDIS_AUTH.

Environment file:

$ nano .env
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

postgres, clickhouse, redis should show healthy (the only ones with configured checks).


First-Run Setup

Visit https://langfuse.example.com:

  1. Sign up — email + password.
  2. Create an organization (e.g. My Company) → Create Organization.
  3. Create a project (e.g. Production LLM) → Create Project.
  4. 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
Enter fullscreen mode Exit fullscreen mode
$ nano test_langfuse.py
Enter fullscreen mode Exit fullscreen mode
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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)