n4nAI

Self-hosting Langfuse on your own infrastructure

Step-by-step guide to self-hosting Langfuse on your own infrastructure with Docker Compose, from provisioning to verifying LLM trace ingestion.

n4n Team3 min read769 words

Audio narration

Coming soon — every post will get a voice note here.

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_URL uses http://, not clickhouse://.
  • 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_MEMORY or 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.

Tagslangfuseself-hostedllm-observabilityopen-source

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llm observability platforms posts →