Bitdoze logo

Langfuse Docker Install: Self-Hosted LangSmith Alternative

Install Langfuse with Docker Compose on your own server. Self-hosted LangSmith alternative: v4 images, .env secrets, Cloudflare Tunnel, upgrades & backups.

Dragos

32 min read

Langfuse Docker Install: Self-Hosted LangSmith Alternative

This guide installs Langfuse v4 with Docker Compose on your own server: a self-hosted LangSmith alternative that keeps LLM traces, costs, and evals on hardware you control. Langfuse is an open-source LLM engineering platform with observability, analytics, prompt management, evaluations, and dataset experiments in one stack. It sits at 35.6k GitHub stars (checked 2026-10-10), and the team has joined ClickHouse, which matters because ClickHouse is the analytics engine under the hood.

The version question first, because it decides everything else: Langfuse v4.0.0 shipped on 2026-07-29 and v4.56.0 landed on 2026-10-09. Langfuse v3 receives security patches only until the end of January 2027. Fresh installs should never start on v3 now. If you already run v3, the upgrade section below walks the migration; the infrastructure does not change.

v3 is end-of-life (security patches only)

Langfuse v3 receives security patches only until the end of January 2027. Fresh installs should use v4 images. Existing v3 installs should plan the upgrade before that date, see Upgrading an existing Langfuse v3 install to v4.

Features:

  • Observability covers instrumenting your application and ingesting traces so you can see how your LLM calls behave, with full-text search across inputs, outputs, and metadata.
  • Analytics tracks cost, latency, and quality through dashboards and data exports.
  • Prompt management versions and deploys prompts from inside Langfuse.
  • Evaluations score completions with model-based evals, user feedback, and manual scoring.
  • Experimentation runs datasets against your app before you ship, and compares runs.
  • Integrations work with LangChain, LlamaIndex, LiteLLM, and other popular LLM frameworks. If you want a single gateway in front of your providers, you can install LiteLLM with Docker Compose and route LLM calls through LiteLLM, tracing them in Langfuse.

What’s new in Langfuse v4 (and why fresh installs should skip v3)

v4 is mostly an engine swap, not a product rewrite. The biggest change is the ClickHouse data model: events land in events_full (immutable, wide rows) with a query-optimized events_core projection on top. Traces are now root spans inside the same table (span ID t-<trace_id>), and the UI main surface is a single Observations view, opened filtered to Is Root Observation = true. If you have screenshots or muscle memory from v3’s traces table, expect a different first screen.

What that data model unlocks for self-hosters (these were Langfuse Cloud features first):

  • Full-text search across inputs, outputs, and metadata, plus a filter search bar.
  • Monitors and alerts on trace metrics.
  • Observations API v2 and Metrics API v2 for cleaner data exports.
  • In-app agent moved out of the enterprise edition into public beta.
  • Better self-hosting ergonomics in the official compose: headless initialization via LANGFUSE_INIT_*, LANGFUSE_AI_* for the in-app assistant and eval models, LANGFUSE_MCP_* for the MCP server, LANGFUSE_S3_BATCH_EXPORT_*, and REDIS_TLS_*.

What's new in v4

  • New ClickHouse model (events_full + events_core), traces as root observations
  • Full-text search, filter search bar, monitors and alerts
  • Observations API v2 and Metrics API v2
  • In-app agent in public beta
  • Headless init, MCP server, batch export env blocks in the official compose
  • SDK direct-write path (Python SDK >= 4.7.0, JS/TS SDK >= 5.4.0)

What stayed the same

  • The same six services: langfuse-web, langfuse-worker, PostgreSQL, ClickHouse, Redis, S3/MinIO
  • The required env vars: NEXTAUTH_URL, NEXTAUTH_SECRET, SALT, ENCRYPTION_KEY, DATABASE_URL, CLICKHOUSE_*, REDIS_*
  • The Docker Compose flow: one compose file, one .env, docker compose up -d
  • AUTH_DISABLE_SIGNUP, telemetry opt-out, health endpoints
  • Only minimum versions changed (ClickHouse >= 25.12, PostgreSQL 15+, Redis 7+)

On the performance numbers

The official upgrade guide claims dashboard load times for large projects improve “10x or more”; the marketing banner says up to 165x. We have not benchmarked these numbers, they come from Langfuse’s own materials checked 2026-10-10. Your mileage depends on hardware and trace volume.

Why skip v3 entirely? It is in maintenance mode with a hard support end date, and v4 runs on the identical infrastructure. The upgrade guide’s own FAQ answers “Do I need new infrastructure components?” with “No.” Same web and worker containers, same PostgreSQL, ClickHouse, Redis, and S3. Starting on v3 today means doing the migration later for no benefit. If you are on v3 already, read the upgrade section and the official v3 → v4 upgrade guide. Release history lives on GitHub releases.

Step-by-step guide to installing Langfuse with Docker Compose

The walkthrough below covers the full Langfuse Docker install: compose file, secrets, verification, HTTPS, and backups. If you prefer video:

Video recorded on v3

The embedded walkthrough was recorded on the v3 UI, so screenshots differ from what you will see on v4. The compose and .env steps are the same; only the image tags and the observations view changed. The article below reflects v4 (checked 2026-10-10).

If you want to watch CPU, memory, and disk while this stack runs, set up how to monitor server and Docker resources first. Langfuse is one of the heavier compose stacks you will self-host, and it will tell you when the box is too small.

1. Prerequisites and VPS sizing for Langfuse v4

Before you start, have these in place:

Sizing: the official example is 4 cores / 16 GiB RAM / 100 GiB disk

Langfuse’s own Docker Compose example uses a 4-core, 16 GiB RAM, 100 GiB disk VM (a t3.xlarge in AWS terms, roughly a Hetzner CPX41-class box, on the order of €15-25/month). Production container guidance is at least 2 CPU and 4 GB RAM per container. An 8 GB box may work for hobby volume, but it is below the official example and we have not tested it. The Node.js heap defaults to 1.7 GiB, so the .env below sets NODE_OPTIONS=–max-old-space-size=4096. Watch memory after deploy with how to monitor server and Docker resources.

VPS prices jumped across the board in 2026. If you are rethinking a rented box, see what changed and when a mini PC wins.

2. Langfuse Docker Compose file (v4 images)

The v4 stack is six services, same shape as v3:

  1. langfuse-worker processes trace events from the queue, writes them to ClickHouse, and runs background jobs including the historic backfill.
  2. langfuse-web serves the UI and public APIs on host port 5061. This is the container your reverse proxy points at.
  3. postgres is the transactional database (PostgreSQL 17) for users, orgs, projects, prompts, and API keys.
  4. clickhouse is the OLAP store for events and analytics. This is where the v4 data model (events_full + events_core) lives.
  5. minio is S3-compatible blob storage for event payloads (events/) and multi-modal media (media/).
  6. redis holds the ingest queue and cache. Losing it costs you queued events, not your database.

Langfuse v4 self-hosted architecture: langfuse-web and langfuse-worker connected to PostgreSQL, Redis, ClickHouse, and MinIO

Create a directory for the stack and drop in this compose file:

bash
mkdir -p /opt/langfuse && cd /opt/langfuse
yaml
services:
  langfuse-worker:
    image: langfuse/langfuse-worker:4
    restart: always
    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 # worker health endpoint (new vs the old v3 file)
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      NEXTAUTH_URL: https://langfuse.yourdomain.com
      SALT: ${SALT}
      ENCRYPTION_KEY: ${ENCRYPTION_KEY}
      NODE_OPTIONS: --max-old-space-size=4096
      TELEMETRY_ENABLED: false
      CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
      CLICKHOUSE_URL: http://clickhouse:8123
      CLICKHOUSE_USER: ${CLICKHOUSE_USER}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
      LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
      LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
      LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
      LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
      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_ROOT_USER}
      LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
      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_PASSWORD}

  langfuse-web:
    image: langfuse/langfuse:4
    restart: always
    depends_on:
      postgres:
        condition: service_healthy
      minio:
        condition: service_healthy
      redis:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
    ports:
      - 5061:3000
    environment:
      DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      NEXTAUTH_URL: https://langfuse.yourdomain.com
      NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
      SALT: ${SALT}
      ENCRYPTION_KEY: ${ENCRYPTION_KEY}
      NODE_OPTIONS: --max-old-space-size=4096
      TELEMETRY_ENABLED: false
      CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
      CLICKHOUSE_URL: http://clickhouse:8123
      CLICKHOUSE_USER: ${CLICKHOUSE_USER}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
      LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
      LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
      LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
      LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
      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_ROOT_USER}
      LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
      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_PASSWORD}
      AUTH_DISABLE_SIGNUP: ${AUTH_DISABLE_SIGNUP}

  postgres:
    image: postgres:17-alpine
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 10s
    volumes:
      - ./langfuse-db:/var/lib/postgresql/data:rw
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

  clickhouse:
    image: clickhouse/clickhouse-server:25.12 # pinned: v4 minimum is 25.12, 26.4 recommended
    restart: always
    user: "101:101"
    ports:
      - 127.0.0.1:8123:8123 # localhost only
      - 127.0.0.1:9000:9000
    environment:
      CLICKHOUSE_DB: default
      CLICKHOUSE_USER: ${CLICKHOUSE_USER}
      CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD}
    volumes:
      - ./langfuse-clickhouse:/var/lib/clickhouse
      - ./langfuse-clickhouse-logs:/var/log/clickhouse-server
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

  minio:
    image: cgr.dev/chainguard/minio
    restart: always
    entrypoint: sh
    command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    ports:
      - 127.0.0.1:9090:9000 # localhost only
      - 127.0.0.1:9091:9001 # MinIO console, localhost only
    volumes:
      - ./langfuse-minio:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 1s
      timeout: 5s
      retries: 5
      start_period: 10s

  redis:
    image: redis:7
    restart: always
    command: >
      --requirepass ${REDIS_PASSWORD}
      --maxmemory-policy noeviction
    ports:
      - 127.0.0.1:6379:6379 # localhost only, for debugging
    volumes:
      - langfuse_redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 3s
      timeout: 10s
      retries: 10
      start_period: 5s

volumes:
  langfuse_redis_data:

What changed vs the old (v3) file

  • Image tags: langfuse/langfuse:3 and langfuse/langfuse-worker:3 become :4.
  • ClickHouse is now pinned to clickhouse/clickhouse-server:25.12 (it was floating on latest, which is exactly the kind of drift that breaks a stack months later).
  • Worker publishes 127.0.0.1:3030 so you can hit its health endpoint.
  • Redis gets a persistent named volume; ClickHouse, MinIO, and Redis ports are bound to localhost instead of the world.
  • MinIO credentials use the official names MINIO_ROOT_USER / MINIO_ROOT_PASSWORD.
  • NODE_OPTIONS raised to 4 GiB to match the container guidance.

A note on registries, since the official compose pulls from docker.langfuse.com:

Docker Hub images

The file above uses langfuse/langfuse:4 and langfuse/langfuse-worker:4 from Docker Hub. Simpler to read, works fine.

Official registry

The official compose pulls docker.langfuse.com/langfuse/langfuse:4. That host is a mirror of Docker Hub (it 307-redirects to registry-1.docker.io), so the images are identical. Use this if your organization allowlists Langfuse’s registry.

Notes on the file:

  • Environment variables (${VARIABLE_NAME}) keep secrets and hostnames out of the YAML. Next section generates them.
  • All backing services have healthchecks with start_period, and the Langfuse containers wait for healthy dependencies before starting.
  • Bind mounts (./langfuse-db, ./langfuse-clickhouse, ./langfuse-minio) keep data on the host filesystem. I prefer bind mounts here over named volumes: they are trivial to tar up in the backups section and trivial to inspect when something breaks.
  • Telemetry is disabled. Langfuse’s compose default is now true, so keep this line if you do not want phone-home stats.
  • S3 prefixes events/ and media/ are set explicitly; the server has the same defaults, so this is documentation more than configuration.
  • For reproducible deployments, pin the exact patch version (for example langfuse/langfuse:4.56.0, current as of 2026-10-10). The :4 tag tracks the current minor and gets patched roughly daily. Watch GitHub releases either way.

The full reference for every variable lives in the official self-hosting configuration docs. The upstream file to diff against is the official Langfuse docker-compose.yml.

3. The .env file: secrets and optional LANGFUSE_INIT variables

Create .env next to the compose file:

bash
POSTGRES_USER='langfuse'
POSTGRES_PASSWORD='change-me-strong'
POSTGRES_DB='langfuse'
NEXTAUTH_SECRET=
SALT=
ENCRYPTION_KEY=
CLICKHOUSE_USER=clickhouse
CLICKHOUSE_PASSWORD=change-me-strong
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=change-me-strong
REDIS_PASSWORD=change-me-strong
AUTH_DISABLE_SIGNUP=false

Generate the three secrets and paste them in. They must not be guessable, and ENCRYPTION_KEY has a fixed format:

  • openssl rand -base64 32 → NEXTAUTH_SECRET
  • openssl rand -base64 32 → SALT
  • openssl rand -hex 32 → ENCRYPTION_KEY (exactly 64 hex characters)
bash
openssl rand -base64 32
openssl rand -base64 32
openssl rand -hex 32

Then lock the file down:

bash
chmod 600 .env

Treat .env like a production secret

Never commit this file to version control and never paste it into a chat. ENCRYPTION_KEY protects sensitive connection credentials: if you lose it, encrypted values in the database cannot be decrypted. Back .env up together with the data volumes (see the backups section) or the backups are only half a restore path.

Optional headless initialization: if you would rather not click through the first-run signup, Langfuse can pre-create the org, project, API keys, and first user from env vars on boot. Add these to .env:

bash
# Optional: headless initialization (skip the UI first-run flow)
# LANGFUSE_INIT_ORG_NAME=My Org
# LANGFUSE_INIT_PROJECT_NAME=default
# LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-...
# LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-...
# LANGFUSE_INIT_USER_EMAIL=you@yourdomain.com
# LANGFUSE_INIT_USER_PASSWORD=change-me-strong

Leave them commented out and the first user gets created from the sign-up page instead. Either path is fine; headless init is better when you automate deployments and want an API key printed to logs rather than clicked together. Details in the headless initialization docs.

4. Deploying the stack and verifying it’s healthy

Start everything from the directory with the compose file:

bash
docker compose up -d

That pulls images, creates containers, and starts them detached. Check they exist:

bash
docker compose ps

All six services should report running, and the four backing services should turn healthy within about a minute.

First boot takes 2-3 minutes

Migrations run on startup. The web container prints Ready when it finishes. Do not restart mid-migration, and do not panic if the UI is blank for the first couple of minutes.

Watch the web container until it is ready:

bash
docker compose logs -f langfuse-web

The success signal is a Ready log line. Then hit the health endpoints:

bash
# web: expect {"success":true}
curl -s http://localhost:5061/api/public/health | jq

# web with DB check: expect {"success":true}
curl -s "http://localhost:5061/api/public/health?failIfDatabaseUnavailable=true" | jq

# readiness: expect 200
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:5061/api/public/ready

# worker: expect {"success":true}
curl -s http://localhost:3030/api/health | jq
  • docker compose ps: six services running, backing services healthy
  • docker compose logs langfuse-web: Ready line present
  • /api/public/health returns {"success":true}
  • curl localhost:3030/api/health returns {"success":true}
  • UI loads at http://your-server-ip:5061 (before the tunnel is up)

The official endpoint reference is the health and readiness endpoints doc. Two failure modes show up here most often: a dependency stuck unhealthy (almost always bad credentials in .env, check docker compose logs postgres or clickhouse), and host port 5061 already taken (ss -tlnp | grep 5061, then change the host port in the compose file). More of these in the troubleshooting section.

5. HTTPS with Cloudflare Tunnels

Cloudflare Tunnels connect your app to the internet without opening inbound ports on the server. The cloudflared daemon dials out to Cloudflare’s network and holds a persistent tunnel to your origin. Requests to your domain come down that tunnel to your server, and responses go back up it. No port forwarding, no firewall holes.

In the Cloudflare Zero Trust dashboard, go to Access - Tunnels, pick your tunnel, and add a hostname that maps your subdomain to the service and port (http://langfuse-web:3000 if the tunnel runs in the same Docker network, or http://localhost:5061 if it runs on the host).

Cloudflare Zero Trust dashboard showing a tunnel hostname routed to Langfuse on port 5061

Verify from your workstation:

bash
curl -I https://langfuse.yourdomain.com

You should get a 302 or 200 pointing at the Langfuse login page, not a Cloudflare error. If you see a 502, the tunnel cannot reach the origin port.

NEXTAUTH_URL must match the public URL

Whatever you set in NEXTAUTH_URL must be exactly the URL users type in the browser, including https. A mismatch is the classic cause of login redirect loops after tunnel setup.

Alternatively, you can use Setup CloudPanel as Reverse Proxy with Docker and Dockge or How to Use Traefik as A Reverse Proxy in Docker.

6. First login: create your admin account (or init headlessly)

No default admin account

Langfuse ships without any login. The first account created through the sign-up page becomes the admin. If signups are already disabled and you have no account, you will be locked out, so create the admin before flipping AUTH_DISABLE_SIGNUP.

Create admin in the UI (default)

  1. Open https://langfuse.yourdomain.com.
  2. Use the sign-up option on the login page. The first user becomes the admin.
  3. Create a project, then open project settings and generate an API key (public + secret key pair).
  4. Point your app’s SDK at the instance and start sending traces.

Headless init via LANGFUSE_INIT_*

  1. Set the LANGFUSE_INIT_* variables in .env before first boot (previous section).
  2. docker compose up -d creates org, project, API keys, and the first user.
  3. Log in with LANGFUSE_INIT_USER_EMAIL / LANGFUSE_INIT_USER_PASSWORD.
  4. Rotate the generated secret key if you generated one outside Langfuse.

Once logged in, the deploy loop is closed: project created, API key generated, ready to trace. In v4 the main screen is the Observations view filtered to root observations, not the old v3 traces table, so older screenshots may look unfamiliar.

You can integrate this with your AI apps next. The embedded video demos the Flowise integration; for the other half of that stack see how to install FlowiseAI with Docker Compose.

Langfuse dashboard after first login with projects, traces, and API keys

Upgrading an existing Langfuse v3 install to v4

If you followed the old version of this guide, the upgrade is three steps and you can pause between them. It is not a fresh install: your data stays in the same bind mounts. Full detail lives in the official v3 → v4 upgrade guide; this is the operator summary.

Langfuse v3 to v4 migration timeline: backup, ClickHouse upgrade, v4 images with backfill, then cutover

Back up before you upgrade

v4 migrations are not reversible by swapping images back to :3. After the schema migrates, a plain v3 image swap fails with no migration found for version 46. Take a full backup (Postgres + ClickHouse + MinIO + .env) before you touch the images. See backing up your Langfuse Docker volumes.

Step 1: upgrade ClickHouse first, while still on v3. v4 needs ClickHouse >= 25.12 (26.4 recommended) for lightweight updates, the JSON type, and full-text search. Current v3 releases are compatible with current ClickHouse, so bump the image on the running v3 stack:

yaml
clickhouse:
  image: clickhouse/clickhouse-server:25.12
bash
docker compose up -d clickhouse
docker compose ps   # clickhouse must go healthy before you continue

Step 2: bump the server images to v4. Change langfuse/langfuse:3 and langfuse/langfuse-worker:3 to :4, then:

bash
docker compose pull
docker compose up -d

Schema migrations run automatically on startup. Historic data is backfilled in the background, newest-first. The backfill is resumable, but it needs scratch space.

Disk headroom for the backfill

The historic backfill uses a scratch table and needs on the order of 3x your current ClickHouse disk usage until it is cleaned up. If the disk is tight, add space first: add a new drive to Ubuntu LVM and mount it permanently.

Write behavior during migration is controlled by env vars you can set on both containers:

Variable Values Default What it controls
LANGFUSE_MIGRATION_V4_WRITE_MODE legacy, dual, events_only events_only Which pipelines accept writes. In dual mode, data from old SDKs appears with roughly 15 minutes of delay (staging pipeline).
LANGFUSE_MIGRATION_V4_NATIVE_OTEL_BEHAVIOUR see upgrade guide - Native OTEL ingestion behavior during migration.
LANGFUSE_MIGRATION_V4_ALLOW_PREVIEW_OPT_IN see upgrade guide - Opt-in for preview ingestion paths.
LANGFUSE_BACKGROUND_MIGRATION_V4_ENABLE_HISTORIC_BACKFILL true, false true Background backfill of historic data into the new model.
LANGFUSE_BACKGROUND_MIGRATION_V4_DROP_PID_TID_SORTING_TABLES true, false see upgrade guide Cleanup of old sorting tables after backfill.

If a dual-write period is running, check the worker propagation probe (expects the 127.0.0.1:3030 port publish from the compose file above):

bash
curl -s "http://localhost:3030/api/health?failIfEventPropagationStuck=true" | jq

A 503 means event propagation has been stuck for more than 15 minutes. Fix that before cutover.

Step 3: cutover, the point of no return. Upgrade your SDKs first: direct-write needs Python SDK >= 4.7.0 and JS/TS SDK >= 5.4.0. Python v2 and JS/TS v3 clients are rejected once you are on the new write path. Then move evaluators to observation-level scoring, switch exports to the “Enriched observations” source, and run on defaults. From here, a v3 rollback is a schema rewind (migrate ... goto 37 from the v4 web image at /app/packages/shared) or a restore from your backup. Prefer the backup.

The infrastructure itself did not change: same web and worker containers, same PostgreSQL, ClickHouse, Redis, and S3. Only the minimum versions moved (ClickHouse 25.12, PostgreSQL 15, Redis 7.0). One honest caveat: this summary follows the official guide, but an in-place ClickHouse upgrade over bind-mounted data deserves a run on staging before you do it on anything you care about.

Troubleshooting: common Langfuse Docker install failures

A dependency container is stuck unhealthy

When docker compose ps shows postgres, clickhouse, minio, or redis as unhealthy, the Langfuse containers never start (they are gated on service_healthy). This is almost always bad credentials in .env, or a volume directory with the wrong ownership (ClickHouse runs as uid 101).

Check docker compose logs postgres (or clickhouse, minio, redis) and read the auth error. For ClickHouse bind mounts: sudo chown -R 101:101 ./langfuse-clickhouse ./langfuse-clickhouse-logs. Then docker compose up -d to retry.

Port 5061 or 3030 is already in use

docker compose up -d fails with Bind for 0.0.0.0:5061 failed: port is already allocated when another container or host service owns the port.

Find the offender with ss -tlnp | grep -E ‘5061|3030’, then either stop it or remap the host port (for example 5062:3000). Keep NEXTAUTH_URL pointed at whatever users browse to.

The web container never prints Ready

Minutes pass and docker compose logs -f langfuse-web shows migrations or errors instead of Ready. Either migrations are still running (give it 2-3 minutes), ClickHouse is older than 25.12 (if you upgraded from v3 without bumping ClickHouse first), or the web container cannot reach one of the backing services.

Check the log tail for the actual error. Confirm ClickHouse answers: curl -s http://localhost:8123/ping should print Ok.. Confirm DATABASE_URL host is postgres (the compose service name), not localhost.

OOM kills or the Node process runs out of heap

Containers restart with exit code 137, or the web UI dies under load. The box is too small (16 GiB is the official example) or the Node heap default of 1.7 GiB is too low for your trace volume.

The compose file above sets NODE_OPTIONS=–max-old-space-size=4096; raise it along with container memory limits if you have the RAM. Watch usage with how to monitor server and Docker resources. If the box is at 8 GB, expect trouble above hobby volume and consider resizing.

The worker queue is wedged

Traces do not appear in the UI, but the API accepts them. The worker stopped consuming from Redis (often after a Redis restart or OOM event).

curl -s “http://localhost:3030/api/health?failIfQueueConsumptionStuck=true” | jq returns 503 if no job has been processed for over 60 minutes. docker compose restart langfuse-worker, then verify new traces land. Queued events from the outage window may be lost, depending on when Redis last persisted.

Login loops back to the sign-up page or redirects wrongly

After the tunnel or reverse proxy is up, logging in bounces you between the login page and an error. The cause is NEXTAUTH_URL not matching the public URL exactly (scheme, host, trailing slash).

Set NEXTAUTH_URL=https://langfuse.yourdomain.com to exactly what users type, then docker compose up -d (an env change is enough; no pull needed).

Securing your self-hosted Langfuse instance

By default, anyone who finds the URL can sign up and create an account. After you create the admin account, flip the flag in .env:

bash
AUTH_DISABLE_SIGNUP=true

Apply the change:

bash
docker compose up -d

Signups off also blocks invites

AUTH_DISABLE_SIGNUP=true blocks any new account creation, including users who have an open project invite. Turn it off temporarily to onboard teammates, then back on. You do not need docker compose pull for an env change; up -d recreates the containers with the new value.

More hardening knobs from the official hardening docs:

  • AUTH_EMAIL_VERIFICATION_REQUIRED=true if you have SMTP configured.
  • AUTH_SESSION_MAX_AGE controls session lifetime (default 43200 minutes, 30 days). Lower it on shared machines.
  • AUTH_DOMAINS_WITH_SSO_ENFORCEMENT and AUTH_DISABLE_USERNAME_PASSWORD=true if you run SSO and want password login gone entirely.
  • Self-hosted Langfuse rejects private or loopback targets for user-configured LLM connections and webhooks by default (SSRF defense). There are *_WHITELISTED_* overrides if you intentionally point at an internal model server.
  • Disable signups after the first admin exists
  • Strong unique values in .env, file mode 600, never committed
  • Telemetry off if the server has restricted egress
  • Session max age lowered to match your threat model
  • Email verification on if SMTP is available
  • Internal service ports bound to 127.0.0.1 (as in the compose file above)

On the whole server level, not just Langfuse: how I secured my Docker server (BSI hardening guide) covers the firewall and Docker-specific holes that matter here.

Cost boundary worth knowing: the core is MIT-licensed and free forever, but SSO/SAML, RBAC, SCIM, and audit logs live in the enterprise edition and require a paid license key. Everything in this guide runs without one.

Backing up your Langfuse Docker volumes

Self-hosting means the data is yours to lose. The stack writes to three bind mounts and one named volume:

Path Holds Backup method Criticality
./langfuse-db PostgreSQL: users, orgs, projects, prompts, API keys pg_dump on a cron Critical
./langfuse-clickhouse (+ -logs) Traces, events, analytics Volume snapshot or clickhouse-backup Critical
./langfuse-minio Event payloads and media blobs Tar or rclone to S3-compatible storage High
langfuse_redis_data Queue and cache Skip; rebuildable Low
.env All secrets including ENCRYPTION_KEY Encrypted offsite copy Critical

ClickHouse backups are your job when self-hosting. The official backup guidance says the same: volume snapshots or clickhouse-backup, your choice. A minimal cron-able script:

bash
#!/usr/bin/env bash
set -euo pipefail
cd /opt/langfuse
set -a; source ./.env; set +a   # export POSTGRES_USER / POSTGRES_DB for pg_dump
STAMP=$(date +%F)
DEST="/backup/langfuse/$STAMP"
mkdir -p "$DEST"

# Postgres logical dump
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB" | gzip > "$DEST/postgres.sql.gz"

# ClickHouse: cold copy of the data dir (stop-free, consistent enough for most self-hosters;
# use clickhouse-backup for stricter consistency)
sudo tar -czf "$DEST/clickhouse.tar.gz" ./langfuse-clickhouse

# MinIO blobs
sudo tar -czf "$DEST/minio.tar.gz" ./langfuse-minio

# Secrets: without these, the volume backups are half a restore
cp .env "$DEST/env.copy"

# Offsite sync (rclone, restic, rsync over SSH, whatever you already trust)
# rclone sync "$DEST" remote:langfuse-backups/"$STAMP"

Backups include .env

Without ENCRYPTION_KEY and the database password, dumps of the volumes cannot be decrypted or loaded. Store .env with the backups, encrypted at rest, in a different failure domain than the VPS.

  • .env included and stored encrypted offsite
  • Postgres pg_dump on a daily cron
  • ClickHouse snapshot or clickhouse-backup weekly, and before every upgrade
  • MinIO volume synced to S3-compatible storage
  • Restore drill on a scratch machine at least once a quarter

Verify the backup, not just the backup job. Restore the three data dirs plus .env into a clean compose directory on another box, run docker compose up -d, and check that your projects and traces appear. A backup you have never restored is a hypothesis. For the offsite side of this, see configure Dokploy backups with Cloudflare R2 (S3-compatible).

Self-hosted Langfuse vs LangSmith: 2026 pricing

The “LangSmith alternative” part of the title is mostly a cost and data-control argument. Numbers as of 2026-10-10:

Self-hosted Langfuse Langfuse Cloud LangSmith
Entry tier Free (MIT core) Hobby: free Developer: $0
Paid tiers License key for EE add-ons only Core $29/mo, Pro $199/mo, Enterprise $2499/mo Plus $39/seat/mo, Enterprise custom
Usage model Unlimited, your hardware Hobby 50k units/mo; Core 100k included, $8/100k over Dev 5k base traces/mo; Plus 10k base traces
Retention Whatever your disk holds Hobby 30 days, Core 90 days, Pro 3 years Plan dependent
Self-hosted option Yes, that is this guide No Enterprise only
Best for Data control, volume, and no per-seat billing Low ops, fast start LangChain shops already paying per seat

Pricing checked 2026-10-10

Verified against langfuse.com/pricing and langchain.com/pricing on 2026-10-10. Pricing pages change; re-check if you are reading this more than a month later.

The cost math for self-hosting is the VPS plus your time. On a Hetzner CPX41-class box (order of magnitude €15-25/month) you get unlimited traces and unlimited users, and the tradeoff is that you own upgrades and backups. Once a team needs SSO/SAML, RBAC, SCIM, or audit logs, those are enterprise-edition features that need a paid license key on top. The core platform in this guide stays free either way.

Conclusion

Self-hosting Langfuse with Docker Compose gives you full control over your data and runs the same infrastructure that powers Langfuse Cloud. Performance depends on your hardware, the official example starts at 4 cores and 16 GiB, and the health endpoints tell you quickly when something is off.

The recap for operators: deploy the six-service v4 stack, verify with /api/public/health and the worker probe, back up the volumes plus .env before you need them, and decide between the floating :4 tag and a pinned langfuse/langfuse:4.56.0 (current 2026-10-10) depending on whether you want automatic patches or reproducible deploys. If you are still on v3, upgrade before the end of January 2027.

One more angle if you use coding agents: the Langfuse organization ships observability plugins that trace Claude Code, Codex, Cursor, OpenCode, and Pi sessions into Langfuse, plus an MCP server (the LANGFUSE_MCP_* env block) that exposes prompt management to agents. It is early days, but tracing agent sessions into the same place as your app traces is useful once you debug multi-step agent runs.

If you want to explore more Docker containers for your home server, including other AI tools, check out the best 125+ Docker containers for home server. Langfuse is also listed with LiteLLM, OmniRoute, and the agent stack in top AI GitHub repos.

Official Langfuse Docs See the v3 to v4 upgrade guide