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.
| Container | Role | Port in docker-compose.yml |
|---|---|---|
| langfuse-web | UI, APIs, SDK ingestion | 3000, all interfaces |
| langfuse-worker | event processing, emails, LLM evaluations | 127.0.0.1:3030 |
| postgres (17) | users, organisations, projects, API keys | 127.0.0.1:5432 |
| clickhouse | traces, observations, scores | 127.0.0.1:8123 and 9000 |
| redis (7) | queue and cache, noeviction policy | 127.0.0.1:6379 |
| minio | raw events, media, batch exports | 9090 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:
| Service | Minimum in the Langfuse sizing guide |
|---|---|
| Web container | 2 CPU, 4 GiB |
| Worker container | 2 CPU, 4 GiB |
| PostgreSQL | 2 CPU, 4 GiB |
| Redis / Valkey | 1 CPU, 1.5 GiB |
| ClickHouse | 2 CPU, 8 GiB |
| MinIO or SeaweedFS | 2 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
- 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.
- As root, turn the firewall on with SSH only:
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable. - Optional: cap the json-file log driver (
max-size,max-file) in/etc/docker/daemon.jsonand restart Docker; the Compose guide warns default logs never rotate. - Run the block below as root. It refuses to run if
/opt/langfuseor its.envis present: running it again would replaceSALT(the salt for hashed API keys) andENCRYPTION_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.
- From your own machine:
ssh -L 3000:127.0.0.1:3000 your-login@your-server, openhttp://localhost:3000, sign up, create the organisation and project, and copy the project's public and secret keys. - 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_URLmust 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. apt-get install -y caddy(packaged in Ubuntu 24.04), then make the file/etc/caddy/Caddyfilehold one site block forlangfuse.example.comcontainingreverse_proxy 127.0.0.1:3000, and check it withcaddy validate --config /etc/caddy/Caddyfile.- 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.