Use case · LLM observability

Langfuse self hosted on an NVMe dedicated server

Langfuse self hosted means running its two application containers (web and worker) plus PostgreSQL, ClickHouse, Redis and an S3-compatible store on hardware you control; the official Docker Compose file starts all six on one host. This page sizes that host from the Langfuse docs, sets it up with every secret replaced and every port on loopback, locks sign-up before HTTPS goes live, and covers what the MIT licence does and does not include.

Configure the server

What the Compose stack runs

Langfuse splits into a web container that serves the UI and the ingestion and public APIs, and a worker that processes events in the background. The architecture page lists four storage services behind them. Incoming traces go to blob storage first, with only a reference queued in Redis, so a slow database does not fail ingestion.

ContainerRolePort in docker-compose.yml
langfuse-webUI, APIs, SDK ingestion3000, all interfaces
langfuse-workerevent processing, emails, LLM evaluations127.0.0.1:3030
postgres (17)users, organisations, projects, API keys127.0.0.1:5432
clickhousetraces, observations, scores127.0.0.1:8123 and 9000
redis (7)queue and cache, noeviction policy127.0.0.1:6379
minioraw events, media, batch exports9090 all interfaces; console 127.0.0.1:9091

The stock file publishes only those two ports to the world; the blob storage guide explains MinIO's part, presigned URLs for media.

Sizing the machine

The Docker Compose guide recommends at least 4 cores and 16 GiB of memory for a VM, and gives 100 GiB as an example disk because observability data grows fast. The sizing page lists minimum requirements per service:

ServiceMinimum in the Langfuse sizing guide
Web container2 CPU, 4 GiB
Worker container2 CPU, 4 GiB
PostgreSQL2 CPU, 4 GiB
Redis / Valkey1 CPU, 1.5 GiB
ClickHouse2 CPU, 8 GiB
MinIO or SeaweedFS2 CPU, 4 GiB

Added up, that is 11 CPUs and 25.5 GiB of memory. That sum is our arithmetic, not a Langfuse figure, and on one host the services share cores, so treat it as a ceiling for planning rather than a requirement. Our reading: 32 GB runs a small team's traces; 64 GB leaves ClickHouse room, which the same guide says scales vertically and wants at least 16 GiB on larger deployments. Disk is the figure that runs out first. The ClickHouse guide suggests alerting at 80% disk use.

Picking the cartridge

The ClickHouse guide notes that it writes and rewrites many files as it merges, and this stack keeps a database, a queue and an object store on the same drives: small random I/O from several processes at once. That is the load the NVMe dedicated server lines are sold for: a whole HPE Moonshot cartridge with its NVMe drives on PCIe, root access and a 1 Gbps unmetered port, so trace volume from your applications never shows up as a transfer bill. Bus and queue figures behind that choice are on NVMe vs SSD vs HDD.

  • Xeon D-1500 line (D-1548 with 8 cores, D-1587 with 16 cores, 32 GB to 128 GB): the fit here.
  • Xeon E3-1585L v5 (4 cores, 32 GB to 64 GB): meets the 4-core floor exactly, nothing spare.
  • Drives: 240 GB to 3.84 TB per drive. Take two in RAID 1 so a failed drive is a swap; the mirror does not replace backups.
  • Delivery: a racked, ready server is live about 30 minutes once payment is approved; a build made to order takes 4 to 24 hours.

Machines and network are ours, on AS19624, in New York, Miami, San Francisco, Amsterdam and Bucharest.

Installing with Docker Compose

  1. Install Ubuntu 24.04. Add Docker Engine and the Compose plugin from Docker's apt repository, as the Langfuse VM guide does. Check git is present.
  2. As root, turn the firewall on with SSH only: sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable.
  3. Optional: cap the json-file log driver (max-size, max-file) in /etc/docker/daemon.json and restart Docker; the Compose guide warns default logs never rotate.
  4. Run the block below as root. It refuses to run if /opt/langfuse or its .env is present: running it again would replace SALT (the salt for hashed API keys) and ENCRYPTION_KEY (used to encrypt sensitive data).
ufw status | grep -q 'Status: active' &&
v=$(docker version -f '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] &&
git clone --depth 1 https://github.com/langfuse/langfuse.git /opt/langfuse &&
cd /opt/langfuse && [ ! -e .env ] &&
k=$(openssl rand -hex 224) && [ ${#k} -eq 448 ] &&
install -m 600 /dev/null .env && cat > .env <<EOF &&
POSTGRES_PASSWORD=${k:0:64}
DATABASE_URL=postgresql://postgres:${k:0:64}@postgres:5432/postgres
CLICKHOUSE_PASSWORD=${k:64:64}
REDIS_AUTH=${k:128:64}
MINIO_ROOT_PASSWORD=${k:192:64}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${k:192:64}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${k:192:64}
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=${k:192:64}
SALT=${k:256:64}
NEXTAUTH_SECRET=${k:320:64}
ENCRYPTION_KEY=${k:384:64}
TELEMETRY_ENABLED=false
EOF
cat > docker-compose.override.yml <<'EOF' &&
services:
  langfuse-web:
    ports: !override
      - 127.0.0.1:3000:3000
    environment:
      AUTH_DISABLE_SIGNUP: ${AUTH_DISABLE_SIGNUP:-false}
  minio:
    ports: !override
      - 127.0.0.1:9090:9000
EOF
cat > /usr/local/sbin/langfuse-portcheck <<'EOF' &&
cd /opt/langfuse && docker compose config --format json | python3 -c 'import json,sys
s=json.load(sys.stdin)["services"]
b=[n for n,v in s.items() for p in v.get("ports",[]) if p.get("host_ip")!="127.0.0.1"]
b and sys.exit("public port on %s" % b)'
EOF
bash /usr/local/sbin/langfuse-portcheck && docker compose up -d

Every change against the official file: the # CHANGEME secrets move into a mode-600 .env that Compose reads from the project folder; TELEMETRY_ENABLED goes to false, allowed on OSS; the web port and MinIO's API port are rebound to 127.0.0.1 with Compose's !override tag; the MinIO console mapping is dropped; and an AUTH_DISABLE_SIGNUP passthrough is added for later. The Python check, saved as /usr/local/sbin/langfuse-portcheck so upgrades can run it too, stops the start if any service would still publish on a public address. A mapping that names no host address is published on 0.0.0.0 and [::], and on IPv4 that traffic skips ufw. Binding to 127.0.0.1 is local-only from Docker Engine 28, which the block also checks. If the block stops after .env exists, do not paste it again; fix the cause and continue from the line that failed.

After two to three minutes the web container should log Ready (check with cd /opt/langfuse && docker compose logs langfuse-web). The .env file is untracked: the repository's .gitignore excludes .env*, so a later git pull leaves it alone.

First account, sign-up off, then HTTPS

Langfuse ships with no default login: the first person to click Sign up creates a user and then an organisation. On a public port anyone could do the same, so create yours privately and close sign-up before the proxy exists.

  1. From your own machine: ssh -L 3000:127.0.0.1:3000 your-login@your-server, open http://localhost:3000, sign up, create the organisation and project, and copy the project's public and secret keys.
  2. On the server, as root: cd /opt/langfuse && ! grep -q '^AUTH_DISABLE_SIGNUP=' .env && printf 'AUTH_DISABLE_SIGNUP=true\nNEXTAUTH_URL=https://langfuse.example.com\n' >> .env && docker compose up -d. NEXTAUTH_URL must match the address people open, so it changes only now. Per the authentication guide, sign-up off also stops invited people without an account from joining; turn it back on briefly when you add someone.
  3. apt-get install -y caddy (packaged in Ubuntu 24.04), then make the file /etc/caddy/Caddyfile hold one site block for langfuse.example.com containing reverse_proxy 127.0.0.1:3000, and check it with caddy validate --config /etc/caddy/Caddyfile.
  4. Open the web ports only once the running container confirms the lockdown: cd /opt/langfuse && [ "$(docker compose exec -T langfuse-web printenv AUTH_DISABLE_SIGNUP)" = true ] && ufw allow 80,443/tcp && systemctl reload caddy. Caddy runs on the host, so without that ufw rule it is blocked on IPv4 and IPv6 alike.

Point your SDKs at https://langfuse.example.com with the project keys. Tracing images or audio needs MinIO reachable too: give it its own Caddy site in front of 127.0.0.1:9090 and set LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT to that public URL, as the blob storage guide describes.

MIT core, ee folders, licence key

The repository's LICENSE file puts everything under the MIT Expat licence except the ee/, web/src/ee/, worker/src/ee/ and packages/shared/src/server/ee/ directories, which fall under ee/LICENSE. The licence-key page says all core features and APIs are in OSS without limits; a key adds project-level RBAC roles, protected prompt labels, data retention policies, audit logs, server-side data masking, UI customisation, organisation creators, the organisation API with SCIM, and the instance management API. The key goes in LANGFUSE_EE_LICENSE_KEY on both containers.

Our general reading, not legal advice: MIT is permissive, so passing copies on, changed or not, means keeping the copyright notice and licence text, with no duty to publish source. ee/LICENSE allows copying and changing that code for development and testing without a subscription; other use needs a valid enterprise agreement. One practical difference: the telemetry page says OSS telemetry can be switched off, while an Enterprise key's licence-compliance telemetry cannot (Government keys send none).

Backups and a gated restore

The Compose guide says plainly that this setup has no backup function, so the script below does it cold: it stops the stack, archives the PostgreSQL, ClickHouse, MinIO and Redis volumes together with .env and the override file, verifies the archive, brings the stack back up, then prunes copies older than seven days (skipped when LF_NO_PRUNE=1). The restart is armed before the stop, so a failed copy still brings Langfuse back. Langfuse is down while it runs, so the cron line puts it at 03:30. The ClickHouse log volume is left out. A raw copy of the PostgreSQL volume only starts under the same PostgreSQL major version (17 in the current file), so restore before changing versions. The archive's .env matters as much as the data: SALT salts the hashed API keys and ENCRYPTION_KEY encrypts sensitive data.

install -d -m 700 /var/backups/langfuse
cat > /usr/local/sbin/langfuse-backup.sh <<'EOF'
set -euo pipefail
umask 077
cd /opt/langfuse
a=/var/backups/langfuse/langfuse-$(date +%F-%H%M%S).tar.gz
trap 'docker compose up -d' EXIT
docker compose stop
tar -czf "$a" -C /var/lib/docker/volumes \
  langfuse_langfuse_postgres_data langfuse_langfuse_clickhouse_data \
  langfuse_langfuse_minio_data langfuse_langfuse_redis_data \
  -C /opt/langfuse .env docker-compose.override.yml
tar -tzf "$a" > /dev/null
docker compose up -d; trap - EXIT
echo "ok $a"
[ "${LF_NO_PRUNE:-0}" = 1 ] || find /var/backups/langfuse -name 'langfuse-*.tar.gz' -mtime +7 -delete
EOF
printf '30 3 * * * root bash /usr/local/sbin/langfuse-backup.sh > /var/log/langfuse-backup.log 2>&1\n' > /etc/cron.d/langfuse-backup

Send the archives to a second machine with plain rsync -a (never --delete) and run the pruning on that machine; a copy on the same drive dies with it. To restore, as root, set A to the archive you want. The block resolves and checks it, takes a fresh backup of the current state first with pruning off, so the archive you are restoring cannot be deleted mid-way (the path printed after ok is your rollback archive), checks the archive again, refuses to run if old .previous copies are still there, moves the current data and config aside, extracts, and starts.

A=$(realpath -e /var/backups/langfuse/langfuse-2026-10-30-033000.tar.gz)
V=/var/lib/docker/volumes; L=/opt/langfuse
tar -tzf "$A" > /dev/null && LF_NO_PRUNE=1 bash /usr/local/sbin/langfuse-backup.sh &&
[ -z "$(ls -d $V/*.previous $L/.env.previous $L/docker-compose.override.yml.previous 2>/dev/null)" ] &&
tar -tzf "$A" > /dev/null && cd $L && docker compose down &&
for s in postgres clickhouse minio redis; do mv $V/langfuse_langfuse_${s}_data{,.previous}; done &&
mv $L/.env{,.previous} && mv $L/docker-compose.override.yml{,.previous} &&
tar -xzpf "$A" -C $V langfuse_langfuse_postgres_data langfuse_langfuse_clickhouse_data \
  langfuse_langfuse_minio_data langfuse_langfuse_redis_data &&
tar -xzpf "$A" -C $L .env docker-compose.override.yml &&
docker compose up -d && echo restored

Check traces and prompts in the UI, then delete the .previous folders and files. To roll back instead: docker compose down, remove the restored volume folders and config, rename the .previous ones back, and docker compose up -d.

On a replacement server the block above cannot run, because its safety backup has nothing to copy. Instead: install Docker and ufw as in the setup steps, clone the repository to /opt/langfuse without starting anything, copy the archive over, run only the two tar -xzpf lines with A, V and L set as above, then cd /opt/langfuse && docker compose up -d. The restored override file and .env carry the loopback ports and the old secrets with them.

Upgrades and limits

Within a major version the image tags langfuse:4 and langfuse-worker:4 follow the latest release, and migrations run on start. Back up, then cd /opt/langfuse && git pull && bash /usr/local/sbin/langfuse-portcheck && docker compose up --pull always -d: pinned images such as ClickHouse move with the file, and nothing restarts if a new release would publish a port. Major jumps follow their own guides; v4 needs ClickHouse 25.12 or newer and PostgreSQL 15 or newer.

The deployment table on the overview page files Docker Compose under local use and testing, and the Compose guide says it lacks high availability, scaling and backups; it grows by moving to a bigger machine. If one server going down must not stop ingestion, the documented path is the Helm chart on Kubernetes across several machines, which our managed Kubernetes runs on whole servers in New York. For ClickHouse on its own box, see self-hosted ClickHouse; if you also want a model behind the playground, the Dify page covers an app stack with Ollama.

Questions

Do I need a licence key to self-host Langfuse?

No. Tracing, prompts, evaluations, datasets and the APIs run in the MIT-licensed core without one. A key is only for the enterprise list: project-level roles, protected prompt labels, retention policies, audit logs, server-side masking, UI customisation and the provisioning APIs. Removing a key later leaves core features working.

Does it need internet access?

The networking page says Langfuse runs without it. The playground and LLM evaluations need an LLM API you can reach, and the update check against GitHub fails quietly when offline. Pulling the images is the one step that needs a registry.

Can I keep traces for a fixed number of days?

Retention policies are an enterprise feature. Without a key, data stays until you delete it, so size the NVMe drive for the history you mean to keep and watch disk use on the ClickHouse volume.

Put Langfuse on its own NVMe cartridge

Pick the Xeon D-1500 line, two drives in RAID 1 and the memory ClickHouse needs; the configurator prices every choice before you pay.

Configure the server

Related on Server Room

Other use cases