Langfuse self-hosted with Docker: trace, debug and evaluate your LLM apps (Linux, Windows, Mac)
Before you start
- 19 min read
Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#
Langfuse is an open-source LLM engineering platform: it records every call your AI app makes (prompt, answer, cost, time), lets you manage prompts and scores the quality, and you can run all of it on your own machine.
Logo: the official Langfuse project (github.com/langfuse/langfuse, file web/public/icon.svg at v4.70.1)
Think of it as a flight recorder for your chatbot, RAG pipeline or agent. When an answer is wrong, you open the trace and see which prompt, which documents and which model call produced it. Your prompts and answers stay on your own server.
At a glance#
- What you get: a web UI with traces, token and cost numbers, sessions, a prompt library with versions, datasets and evaluation scores.
- Time needed: about 20 minutes, plus 2 to 3 minutes for the first start.
- Difficulty: medium. Six containers, but you copy one Compose file and one secrets script.
- You need: Docker with the Compose plugin, 4 CPU cores and 16 GB RAM recommended by the docs, and a browser.
| What | Value |
|---|---|
| Current version | v4.70.1, released 26 Oct 2026 (GitHub releases) |
| Pinned images (official, Docker Hub) | langfuse/langfuse:4.70.1 and langfuse/langfuse-worker:4.70.1 |
| Other images | ClickHouse 25.12, Postgres 17, Redis 7, MinIO |
| Ports | 3000 (web and API), 9090 (MinIO, for media uploads) |
| Official docs | langfuse.com/self-hosting |
Why this version? Langfuse ships almost daily (v4.69.0 on 23 Oct, v4.70.0 on 24 Oct, v4.70.1 on 26 Oct), so we pin one exact tag and you update on purpose. At the time of writing our research service had not yet returned the community breakage check, so we only checked that the tag exists on Docker Hub and that the official Compose file at this tag matches this guide. If a release misbehaves for you, set the tag to the previous release (for example 4.70.0).
Already running a gateway? Our LiteLLM guide shows how to put one OpenAI-compatible endpoint in front of your models; this guide shows how to watch it.
How it fits together#
Your app sends traces to langfuse-web, which stores the raw events and queues them; langfuse-worker takes them off the queue and writes them into ClickHouse, where the UI reads them.
Remember three things:
- Only langfuse-web (port 3000) needs to be reachable by your apps and your browser.
- Postgres, ClickHouse, Redis and MinIO are private. The official file already binds most of them to
127.0.0.1. - Traces are written in the background, so they show up a few seconds after the call.
Before you start#
You need Docker Compose and enough memory. ClickHouse is the hungry part.
| System | What to install |
|---|---|
| Linux | Docker Engine + Compose plugin, openssl |
| Windows | Docker Desktop (WSL2 backend), PowerShell. Give WSL2 enough RAM in .wslconfig |
| Mac | Docker Desktop, Terminal. Allow at least 8 GB for Docker in its settings |
docker --version docker compose version
Checklist:
- [ ] Port 3000 is free
- [ ] You have 16 GB RAM free for a comfortable setup (docs recommendation for a VM)
- [ ] You can save secrets in a
.envfile that only you can read
Tested on 2026-10-26: not run. Commands checked against the official Langfuse docs and the official docker-compose.yml at tag v4.70.1.
Step 1: Create the folder and the secrets#
The official file marks every password with # CHANGEME. We move them all into one .env file and generate each with a random generator.
Here is the full list of secrets from the official file, and what we do with each:
| Secret in the official file | How to make it | In our .env |
|---|---|---|
NEXTAUTH_SECRET (default mysecret) | openssl rand -base64 32 | NEXTAUTH_SECRET |
SALT (default mysalt) | openssl rand -base64 32 | SALT |
ENCRYPTION_KEY (64 zeros) | openssl rand -hex 32 (must be 64 hex characters) | ENCRYPTION_KEY |
DATABASE_URL (password postgres) and POSTGRES_PASSWORD | openssl rand -hex 24 | POSTGRES_PASSWORD (the URL is built from it) |
CLICKHOUSE_PASSWORD (clickhouse) | openssl rand -hex 24 | CLICKHOUSE_PASSWORD |
REDIS_AUTH (myredissecret) | openssl rand -hex 24 | REDIS_AUTH |
MINIO_ROOT_PASSWORD and the three LANGFUSE_S3_*_SECRET_ACCESS_KEY (miniosecret) | openssl rand -hex 24 | MINIO_ROOT_PASSWORD (all three S3 secrets reuse it) |
Never change ENCRYPTION_KEY later: it encrypts the LLM API keys you save in the UI.
Linux#
mkdir -p ~/docker_volumes/langfuse && cd ~/docker_volumes/langfuse cat > .env <<EOF NEXTAUTH_URL=http://localhost:3000 NEXTAUTH_SECRET=$(openssl rand -base64 32) SALT=$(openssl rand -base64 32) ENCRYPTION_KEY=$(openssl rand -hex 32) POSTGRES_PASSWORD=$(openssl rand -hex 24) CLICKHOUSE_PASSWORD=$(openssl rand -hex 24) REDIS_AUTH=$(openssl rand -hex 24) MINIO_ROOT_PASSWORD=$(openssl rand -hex 24) EOF chmod 600 .env
Windows (PowerShell)#
Windows has no openssl by default, so we use .NET's random generator. It works in Windows PowerShell and PowerShell 7.
$d = "$HOME\docker_volumes\langfuse"
New-Item -ItemType Directory -Force $d | Out-Null
Set-Location $d
function Hex($n) { $b = New-Object byte[] $n; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); ($b | ForEach-Object { $_.ToString('x2') }) -join '' }
function B64($n) { $b = New-Object byte[] $n; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($b); [Convert]::ToBase64String($b) }
@(
"NEXTAUTH_URL=http://localhost:3000",
"NEXTAUTH_SECRET=$(B64 32)",
"SALT=$(B64 32)",
"ENCRYPTION_KEY=$(Hex 32)",
"POSTGRES_PASSWORD=$(Hex 24)",
"CLICKHOUSE_PASSWORD=$(Hex 24)",
"REDIS_AUTH=$(Hex 24)",
"MINIO_ROOT_PASSWORD=$(Hex 24)"
) | Set-Content -Encoding ascii .env
Mac (Terminal)#
Same commands as Linux (macOS ships openssl).
Check it worked#
grep -c "=" .env
Expected: 8. On Windows use (Get-Content .env).Count. Do not print the file in a shared screen.
Step 2: Save the Compose file#
This file is the official one at v4.70.1, trimmed to what a first install needs, with exact image tags, the secrets taken from .env, and the web port bound to localhost.
Everything we removed is an empty optional setting (SMTP, cloud object storage, AI features, allow-lists). Add them back from the official file if you need them. Open an editor in the same folder, paste the file below and save it as docker-compose.yml:
Linux#
cd ~/docker_volumes/langfuse && nano docker-compose.yml
Windows (PowerShell)#
Set-Location $HOME\docker_volumes\langfuse; notepad docker-compose.yml
Mac (Terminal)#
Same command as Linux.
The Compose file#
name: langfuse
services:
langfuse-worker:
image: langfuse/langfuse-worker:4.70.1
restart: unless-stopped
depends_on: &langfuse-depends-on
postgres:
condition: service_healthy
minio:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
ports:
- 127.0.0.1:3030:3030
environment: &langfuse-env
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:3000}
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:?set it in .env}@postgres:5432/postgres
SALT: ${SALT:?set it in .env}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?set it in .env}
TELEMETRY_ENABLED: ${TELEMETRY_ENABLED:-true}
CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
CLICKHOUSE_URL: http://clickhouse:8123
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?set it in .env}
CLICKHOUSE_CLUSTER_ENABLED: "false"
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:?set it in .env}
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:?set it in .env}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/
REDIS_HOST: redis
REDIS_PORT: "6379"
REDIS_AUTH: ${REDIS_AUTH:?set it in .env}
REDIS_TLS_ENABLED: "false"
langfuse-web:
image: langfuse/langfuse:4.70.1
restart: unless-stopped
depends_on: *langfuse-depends-on
ports:
- 127.0.0.1:3000:3000
environment:
<<: *langfuse-env
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:?set it in .env}
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9090
LANGFUSE_S3_MEDIA_UPLOAD_INTERNAL_ENDPOINT: http://minio:9000
clickhouse:
image: docker.io/clickhouse/clickhouse-server:25.12
restart: unless-stopped
user: "101:101"
environment:
CLICKHOUSE_DB: default
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD:?set it in .env}
volumes:
- langfuse_clickhouse_data:/var/lib/clickhouse
- langfuse_clickhouse_logs:/var/log/clickhouse-server
ports:
- 127.0.0.1:8123:8123
- 127.0.0.1:9000:9000
healthcheck:
test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
interval: 5s
timeout: 5s
retries: 10
start_period: 1s
minio:
image: cgr.dev/chainguard/minio
restart: unless-stopped
entrypoint: sh
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?set it in .env}
ports:
- 127.0.0.1:9090:9000
- 127.0.0.1:9091:9001
volumes:
- langfuse_minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 1s
timeout: 5s
retries: 5
start_period: 1s
redis:
image: docker.io/redis:7
restart: unless-stopped
command: >
--requirepass ${REDIS_AUTH:?set it in .env}
--maxmemory-policy noeviction
ports:
- 127.0.0.1:6379:6379
volumes:
- langfuse_redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 10s
retries: 10
postgres:
image: docker.io/postgres:17
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set it in .env}
POSTGRES_DB: postgres
TZ: UTC
PGTZ: UTC
ports:
- 127.0.0.1:5432:5432
volumes:
- langfuse_postgres_data:/var/lib/postgresql/data
volumes:
langfuse_postgres_data:
langfuse_clickhouse_data:
langfuse_clickhouse_logs:
langfuse_minio_data:
langfuse_redis_data:
What we changed from the official file, and why:
| Change | Reason |
|---|---|
Exact image tags 4.70.1 (official: floating 4) | You decide when to update |
127.0.0.1:3000 and 127.0.0.1:9090 (official: all interfaces) | Nothing is public until you add HTTPS. If other machines must send traces, see the HTTPS section |
restart: unless-stopped (official: always) | Same house style as our other guides |
${VAR:?...} for every secret | Compose refuses to start with a missing secret, instead of silently using postgres or mysecret |
| Data in named Docker volumes (as in the official file) | ClickHouse and MinIO run as their own users; named volumes avoid permission trouble. The folder ~/docker_volumes/langfuse holds the Compose file, .env and backups |
Check it worked#
cd ~/docker_volumes/langfuse docker compose config --quiet && echo OK
On Windows run the same two commands in PowerShell after Set-Location $HOME\docker_volumes\langfuse (use ; if ($?) { "OK" }). Expected: OK. If a secret is missing, Compose stops with an error that names it.
Step 3: Start Langfuse#
Linux#
docker compose up -d docker compose logs -f langfuse-web
Windows (PowerShell)#
docker compose up -d docker compose logs -f langfuse-web
Mac (Terminal)#
Same commands as Linux.
Check it worked#
Wait 2 to 3 minutes. The docs say langfuse-web then logs "Ready" (press Ctrl+C to leave the log). Then:
docker compose ps
Expected: six services running, and postgres, redis, clickhouse and minio marked healthy. Open http://localhost:3000: you see the Langfuse sign-in page.
First run: your account and API keys#
There is no default login. The first account you create becomes your user, then you create an organization and a project, and the project gives you the API keys.
- Open
http://localhost:3000and click Sign up. Use your email and a strong password. - Create an organization, then a project.
- Open Settings > API Keys > Create new API key.
- Copy the public key (
pk-lf-...) and the secret key (sk-lf-...). The secret key is shown once.
Want no clicking? "Headless initialization" creates the organization, project, keys and first user from LANGFUSE_INIT_* variables on langfuse-web (see the docs). After your own account exists you can also stop strangers from signing up: set AUTH_DISABLE_SIGNUP=true on langfuse-web (listed in the configuration reference; VERIFY the name in your version).
Check it worked#
You see an empty project with a Tracing page and your keys saved in a password manager.
Step 5: Send your first trace from Python#
Install the Langfuse SDK, give it your keys and your server address, and swap one import.
Linux#
python3 -m venv .venv && source .venv/bin/activate pip install langfuse openai export LANGFUSE_PUBLIC_KEY="<YOUR_PUBLIC_KEY>" export LANGFUSE_SECRET_KEY="<YOUR_SECRET_KEY>" export LANGFUSE_BASE_URL="http://localhost:3000"
Windows (PowerShell)#
py -m venv .venv .\.venv\Scripts\Activate.ps1 pip install langfuse openai $env:LANGFUSE_PUBLIC_KEY = "<YOUR_PUBLIC_KEY>" $env:LANGFUSE_SECRET_KEY = "<YOUR_SECRET_KEY>" $env:LANGFUSE_BASE_URL = "http://localhost:3000"
Mac (Terminal)#
Same commands as Linux.
Save this as trace_test.py. The Langfuse OpenAI wrapper is a drop-in replacement for the OpenAI client. It works with any OpenAI-compatible server, for example a local Ollama or LiteLLM (set base_url):
from langfuse import get_client
from langfuse.openai import OpenAI # drop-in for openai.OpenAI
client = OpenAI(base_url="http://localhost:4000/v1", api_key="<YOUR_LLM_KEY>") # your OpenAI-compatible endpoint
reply = client.chat.completions.create(
model="<your-model-name>",
messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(reply.choices[0].message.content)
get_client().flush() # send traces before the script exits
python trace_test.py
Check it worked#
Open Tracing in your project. A trace with the prompt, the answer, the token counts and the time appears within a few seconds. If nothing shows up, see Troubleshooting.
Send traces from LiteLLM, n8n and Open WebUI#
Pick the layer you already have: a gateway, a workflow tool or a chat UI.
| Tool | How | Notes |
|---|---|---|
| LiteLLM Proxy | callbacks: ["langfuse_otel"] under litellm_settings, plus LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY and LANGFUSE_OTEL_HOST as environment variables | Set LANGFUSE_OTEL_HOST to your server, for example http://localhost:3000. If LiteLLM runs in Docker, localhost is the container: use your machine's address or put both on one Docker network |
| n8n | No native tracing integration. Use a prompt-management node, or send LLM calls through a gateway (LiteLLM) or OpenRouter's Broadcast feature | Per the Langfuse n8n page, read 2026-10-26 |
| Open WebUI | The Langfuse docs have an Open WebUI integration page | Follow that page for your Open WebUI version (VERIFY) |
LiteLLM config snippet (from the official Langfuse LiteLLM page):
litellm_settings: callbacks: ["langfuse_otel"]
Because LiteLLM sits in front of every model, one callback traces them all. That is the cheapest way to get coverage across n8n, scripts and chat tools.
Prompts and evaluations#
Prompt management keeps your prompts out of your code and versioned; evaluations score the answers.
- Prompt management: create a prompt in the UI (Prompts > New prompt), label a version
production, and fetch it from code withget_client().get_prompt("<name>"). Changing the prompt no longer needs a redeploy, and every trace shows which prompt version produced it. - Scores: attach a number or label to a trace from code, from the UI (human annotation) or with an LLM-as-a-judge evaluator.
- Datasets and experiments: save test inputs with expected outputs, run your app over them, and compare runs side by side after each change.
- LLM-as-a-judge needs a model connection: add your own API key under Settings > LLM Connections. It is stored encrypted with your
ENCRYPTION_KEY.
For RAG apps, trace the retrieval step too, so you can see which chunks the model was given when an answer is wrong.
Docs: prompt management, evaluation.
How Langfuse compares#
Langfuse is the open-source, self-hostable all-rounder; LangSmith is a hosted product from the LangChain team; Phoenix is a lighter open-source tool focused on tracing and evaluation.
| Langfuse | LangSmith | Arize Phoenix | |
|---|---|---|---|
| Self-host | Yes, Docker Compose or Kubernetes | Enterprise option (check with the vendor) | Yes, single container |
| Prompt management | Built in | Built in | Available |
| Evaluations | Datasets, scores, LLM judge | Yes | Yes |
| Stack to run | Six containers | n/a (hosted) | Fewer parts |
| Best for | Teams wanting one open platform on their own hardware | LangChain-centred teams | Quick local debugging |
The Langfuse column comes from its docs. The other two columns are our general summary, not checked against their current docs on this date. Check them before you choose.
Put it behind HTTPS (with or without Traefik)#
Use this when other machines must reach Langfuse, or you want a proper address. Running only on one computer? Skip it: http://localhost:3000 is fine.
Before you start: set NEXTAUTH_URL in .env to your final address, for example https://langfuse.example.com. Langfuse uses it for login redirects and links. Then run docker compose up -d again.
With Traefik#
Prerequisites: a domain, DNS pointing to the server, ports 80 and 443 open, and Traefik on the external proxy network.
docker network create proxy
Add this to the langfuse-web service (and replace its ports: line, so the port is no longer published). The labels are our example, not from the official docs (VERIFY against your Traefik setup):
langfuse-web:
networks:
- default
- proxy
labels:
- traefik.enable=true
- traefik.docker.network=proxy
- traefik.http.routers.langfuse.rule=Host(`<your-domain>`)
- traefik.http.routers.langfuse.entrypoints=https
- traefik.http.routers.langfuse.tls=true
- traefik.http.services.langfuse.loadbalancer.server.port=3000
networks:
proxy:
external: true
Without Traefik: Caddy#
Choose Caddy if you have no Traefik yet and run one small server.
| Case | Result | You do |
|---|---|---|
| Public domain | Automatic Let's Encrypt | DNS A record to the server, ports 80 and 443 open |
| Home only | tls internal (local certificate) | Trust Caddy's root certificate on each device |
Caddyfile for a public domain (Langfuse keeps its port on 127.0.0.1:3000, as in our file):
langfuse.example.com {
reverse_proxy localhost:3000
}
Caddyfile for home only (point the name at the Caddy machine):
langfuse.home.arpa {
tls internal
reverse_proxy localhost:3000
}
Trust the root certificate (pki/authorities/local/root.crt in Caddy's data directory):
# Linux: on the Caddy machine sudo caddy trust # other Linux devices, with root.crt copied over sudo cp root.crt /usr/local/share/ca-certificates/caddy-root.crt && sudo update-ca-certificates # Windows (PowerShell as administrator) certutil -addstore -f ROOT root.crt # Mac sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain root.crt
Run Caddy (Linux: install from the official apt repository on caddyserver.com/docs/install first):
# Linux sudo systemctl reload caddy # Windows (PowerShell) winget install CaddyServer.Caddy caddy run # Mac brew install caddy && caddy run
Or use Docker. Caddy then has to reach Langfuse on the Docker network: put it on the proxy network, give langfuse-web the proxy network (as above, without labels), and use reverse_proxy langfuse-web:3000. Save this in ~/docker_volumes/caddy:
services:
caddy:
image: caddy:2.11.7
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
networks:
- proxy
networks:
proxy:
external: true
Caddy passes WebSockets and streaming by default. Set NEXTAUTH_URL to the https address and run docker compose up -d.
Check it worked: curl -I https://langfuse.example.com answers HTTP/2 200 or a redirect to /auth/sign-in.
Two extra points for remote use:
- Your apps then send traces to
https://<your-domain>(setLANGFUSE_BASE_URLto it). - Media uploads (images and files in traces) go from the browser straight to MinIO on
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT. Behind a domain this must be an address the browser can reach, such as a second hostname for MinIO. Text-only tracing works without it.
Security and testing#
Keep the databases private, keep secrets in .env, and put HTTPS in front before anything leaves your network.
- Never forward 3000, 5432, 6379, 8123, 9000 or 9090 to the internet. In our file all but the web port are bound to
127.0.0.1. Reach it from outside through a VPN such as WireGuard or Tailscale, or through HTTPS as above. - Replace every default. The official file starts with weak placeholder passwords. Our
.envmethod removes all of them. - Lock the file:
chmod 600 .env(Windows: keep the folder inside your user profile). - Telemetry: the file keeps the official default
TELEMETRY_ENABLED=true. Set it tofalsein the.envand the Compose file if you want no anonymous usage statistics (see the telemetry page). - Sign-ups: after your account exists, turn open sign-up off (first-run section).
Tests:
docker compose ps docker compose logs langfuse-web --tail 50 curl -I http://localhost:3000
Expected: all services running, no errors in the log, and a response from the last command (HTTP/1.1 200 OK, or a redirect to the sign-in page).
Troubleshooting#
| Problem | Solution |
|---|---|
| Compose stops before starting | A secret is missing in .env. The error names it. Add it and run again |
Page does not load right after up -d | First start runs database migrations. Wait 2 to 3 minutes and watch docker compose logs -f langfuse-web for "Ready" |
langfuse-worker or web keeps restarting | Run docker compose logs langfuse-worker --tail 100. Usually ClickHouse, Redis or Postgres is not healthy yet, or a password in .env changed after the first start |
| Changed a password and now nothing connects | The database keeps the first password. Change it inside the database, or delete the volume (this erases data) and start fresh |
| No traces appear | Check LANGFUSE_BASE_URL (no trailing path), the keys, and call get_client().flush() in short scripts. Check docker compose logs langfuse-web for 401 errors (wrong key) |
| Traces arrive late | Events wait in Redis until the worker writes them. Check the worker is running |
| LiteLLM in Docker cannot reach Langfuse | localhost is the container. Use your machine's address, or put both on the same Docker network |
| Login redirects to the wrong address | NEXTAUTH_URL must be the exact address in your browser, including https:// |
| Out of memory, containers killed | ClickHouse needs RAM. Give Docker or WSL2 more, or move to a larger machine |
Exact error: missing secret#
required variable POSTGRES_PASSWORD is missing a value: set it in .env
Compose reads .env from the folder you run it in. cd into ~/docker_volumes/langfuse first and check the line exists.
Exact error: port in use#
Bind for 127.0.0.1:3000 failed: port is already allocated
Something else uses port 3000. Stop it, or change the left side of 127.0.0.1:3000:3000 (and NEXTAUTH_URL).
Known bugs#
We did not complete a review of the open issues for v4.70.1 (the research service had not answered yet). Check github.com/langfuse/langfuse/issues before you update, and roll back by setting the previous tag.
Updates and backups#
Update by changing the tag, back up by stopping the stack and copying the volumes.
Update:
cd ~/docker_volumes/langfuse # read the release notes, edit both Langfuse image tags to the new version, then: docker compose pull && docker compose up -d
Windows: the same two lines in PowerShell after Set-Location $HOME\docker_volumes\langfuse. The docs say the database migrations run on start. The official Compose file has no backup feature, so we add one.
Backup script (Linux/Mac). It stops the stack briefly so the databases are consistent:
cd ~/docker_volumes/langfuse
mkdir -p backups
docker compose stop
for v in postgres clickhouse minio redis; do
docker run --rm -v langfuse_langfuse_${v}_data:/v:ro -v "$PWD/backups":/b alpine \
tar czf /b/${v}-$(date +%F).tgz -C /v .
done
cp .env backups/env-$(date +%F).bak && chmod 600 backups/env-$(date +%F).bak
docker compose start
Windows (PowerShell):
Set-Location $HOME\docker_volumes\langfuse
New-Item -ItemType Directory -Force backups | Out-Null
docker compose stop
$day = Get-Date -Format yyyy-MM-dd
foreach ($v in "postgres","clickhouse","minio","redis") {
docker run --rm -v "langfuse_langfuse_${v}_data:/v:ro" -v "${PWD}\backups:/b" alpine tar czf "/b/$v-$day.tgz" -C /v .
}
Copy-Item .env "backups\env-$day.bak"
docker compose start
The volume names are langfuse_ (project name) plus the volume name from the file. Confirm yours with docker volume ls. Keep the .env copy safe: without the same ENCRYPTION_KEY the saved LLM keys cannot be read.
Schedule: Linux/Mac cron at 03:00 (0 3 * * * <path-to-script>). Windows: Task Scheduler running the PowerShell steps.
Restore test#
Do this once, before you need it. It reads a backup into a throwaway volume and lists it:
docker volume create restore_test docker run --rm -v restore_test:/v -v ~/docker_volumes/langfuse/backups:/b alpine sh -c 'tar xzf /b/postgres-*.tgz -C /v && ls /v | head' docker volume rm restore_test
Expected: a list of Postgres files such as PG_VERSION. For a full restore, stop the stack, remove the real volume, create it again, extract the archive into it and start the stack.
Performance and resources#
Plan for 4 cores and 16 GB RAM on a VM, per the official Docker Compose page. We did not measure RAM or CPU ourselves. Measure on your own machine:
docker stats --no-stream
Tips:
- ClickHouse and the worker use the most memory as traces pile up. Watch them first.
- The docs say the Docker Compose setup has no high availability or scaling. For heavy traffic use Kubernetes (Helm).
- Keep traces small: very long prompts and images make the databases and MinIO grow fast. Set a retention period in the project settings if you only need recent data.
- Redis uses
--maxmemory-policy noevictionon purpose: do not change it.
FAQ#
Is Langfuse free to self-host? Yes, the core platform is open source and free to run yourself. Some enterprise features need a license key (see the License Key page in the docs).
Do I need all six containers? Yes for v4: web, worker, Postgres, ClickHouse, Redis and MinIO (or another S3 store). The worker and ClickHouse are what make it fast.
Which image tag should I use? The pinned 4.70.1. The official file uses the floating 4; we pin so updates are your choice. Use the previous release only to roll back.
Does it work on Windows and Mac? Yes, through Docker Desktop. Use at least 8 GB for Docker on a Mac and a generous .wslconfig on Windows.
Can I use it with a local model? Yes. Any app that talks to an OpenAI-compatible server (Ollama, LiteLLM, vLLM) can be traced with the OpenAI wrapper or with LiteLLM's callback.
Does it only work with Python? No. There are SDKs for Python and JavaScript/TypeScript, and an OpenTelemetry endpoint for other languages.
Sources#
- Caddy docs: automatic HTTPS, tls internal, reverse_proxy, install (Caddy 2.11.7, read 2026-10-26)
- Langfuse self-hosting: Docker Compose, configuration and headless initialization (v4 docs, read 2026-10-26)
- Official docker-compose.yml at v4.70.1 and releases (read 2026-10-26)
- Langfuse tracing quickstart, LiteLLM integration, n8n integration, Open WebUI integration (read 2026-10-26)
- Docker Hub: langfuse/langfuse and langfuse/langfuse-worker
Checked against the official docs on 2026-10-26. Not run by DIY Lab Hub. Items marked VERIFY are not confirmed in the official docs.