Workload · Tracing an AI app with Langfuse

Langfuse self hosted: your LLM traces on a server of your own

To run Langfuse self hosted, you start its official Docker Compose stack, six containers in all, on one Linux server, change every default password before the first start, and put HTTPS in front of the web container only. Here is how much machine that takes, a setup that keeps the databases private, how to stop strangers signing up, which features are free under MIT, and a backup you can actually restore.

The short version
  1. One server runs the whole official stack
  2. Langfuse's single-machine floor: four cores, 16 GiB RAM
  3. Every default secret replaced before first start
  4. Sign-up switched off before HTTPS opens
  5. Backup and restore scripts with a rollback copy

What Langfuse does for your AI app

Langfuse records what your app asks a language model and what comes back, as traces you can search, score and evaluate, and it stores versioned prompts your code fetches. Hosting it yourself keeps all of that on hardware you rent.

The Docker Compose file wires six containers together:

  • Web: the screens you use, the public API and the endpoint your SDKs send traces to.
  • Worker: picks queued events up and writes them to the database, and runs evaluations.
  • PostgreSQL: your users, organisations, projects and API keys.
  • ClickHouse: the traces, observations and scores, which is where the space goes.
  • Redis: the queue and a cache.
  • MinIO: S3-style file storage; every incoming event lands here first, so a busy database doesn't lose it.

How much server does it need?

For a single machine, Langfuse's Compose guide asks for a minimum of four cores plus 16 GiB of RAM, and suggests something like 100 GiB of disk, because trace data piles up quickly. Its sizing page goes further and gives a minimum for each service in a split-out production setup:

PieceWhat it holds or doesIts documented minimum
Web and workerscreens, API, background jobs2 CPU and 4 GiB each
PostgreSQLaccounts and keys2 CPU, 4 GiB
ClickHousetrace data2 CPU, 8 GiB
Redisqueue and cache1 CPU, 1.5 GiB
MinIOraw events and files2 CPU, 4 GiB

Added up by us, that's 11 CPUs plus 25.5 GiB, but on one box the pieces share. Our rule of thumb, not Langfuse's: 32 GB gets you going, 64 GB lets ClickHouse breathe, since the sizing page says it speeds up with memory and wants 16 GiB or more on bigger installs. Size the drive for how long you'll keep traces.

The server that fits

An NVMe dedicated server is a good home for this job, because ClickHouse is busy rewriting files on disk as it merges data, and PostgreSQL, Redis and MinIO all share the same drive. NVMe vs SSD vs HDD explains why PCIe drives cope better with many small reads and writes. You rent the whole HPE Moonshot cartridge, the drive is yours alone, and the racked ones show the read speed our benchmark measured on them before you pay.

  • Choose the Intel Xeon D-1500 line: 8 cores on the D-1548 or 16 on the D-1587, with 32 GB to 128 GB of memory.
  • Mirror two NVMe drives with RAID 1: if one dies, it gets swapped and your traces stay put. It's still no substitute for a backup.
  • Your apps can send as many traces as they like: nothing meters the 1 Gbps port and there is no overage bill.
  • A racked machine is yours about 30 minutes after your order is approved. A build you configure takes 4 to 24 hours when we hold the parts, and longer when one has to be bought in.

There's no contract, and you can pick Miami, New York, San Francisco, Amsterdam or Bucharest.

Up and running in an evening

  1. Install Ubuntu 24.04 on the server, then install Docker's engine plus its Compose plugin, using the apt repository Docker publishes, exactly as Langfuse's VM walkthrough shows. Make sure git is installed.
  2. If you want container logs capped, set max-size and max-file for Docker's json-file driver (the file is /etc/docker/daemon.json), then restart Docker; Langfuse warns that the default never rotates them.
  3. Paste the block below as root. It turns the firewall on with only SSH allowed, then fetches Langfuse into /srv/langfuse and writes fresh random secrets into a .env file only root can read.
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw &&
ufw allow OpenSSH && ufw --force enable &&
v=$(docker version -f '{{.Server.Version}}') && [ "${v%%.*}" -ge 28 ] &&
git clone --depth 1 https://github.com/langfuse/langfuse.git /srv/langfuse &&
cd /srv/langfuse && [ ! -e .env ] &&
r=$(openssl rand -hex 224) && [ ${#r} -eq 448 ] &&
install -m 600 /dev/null .env && cat > .env <<EOF &&
SALT=${r:0:64}
NEXTAUTH_SECRET=${r:64:64}
ENCRYPTION_KEY=${r:128:64}
POSTGRES_PASSWORD=${r:192:64}
DATABASE_URL=postgresql://postgres:${r:192:64}@postgres:5432/postgres
CLICKHOUSE_PASSWORD=${r:256:64}
REDIS_AUTH=${r:320:64}
MINIO_ROOT_PASSWORD=${r:384:64}
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${r:384:64}
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${r:384:64}
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=${r: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/lf-portcheck <<'EOF' &&
cd /srv/langfuse && docker compose config --format json | python3 -c 'import json,sys
svc=json.load(sys.stdin)["services"]
pub=[n for n,c in svc.items() for p in c.get("ports",[]) if p.get("host_ip")!="127.0.0.1"]
pub and sys.exit("still public: %s" % pub)'
EOF
bash /usr/local/sbin/lf-portcheck && docker compose up -d

It stops if /srv/langfuse or its .env is already there. That's on purpose: running it twice would swap SALT, which salts the stored hashes of your API keys, and ENCRYPTION_KEY, which encrypts the sensitive data Langfuse stores. If it does stop after writing .env, don't paste it again: fix the error and run the remaining lines from the one that failed.

Every change it makes to the stock setup: the # CHANGEME passwords come from .env, which Compose reads from that folder and Langfuse's .gitignore keeps out of git. Telemetry is off, which the free edition allows. The web and MinIO ports, the only two the official file opens to the world, are rebound to the loopback address 127.0.0.1 using the !override merge tag from Compose. The MinIO console port is removed, and a switch for sign-up is added for the next step.

Then a short Python check, kept as /usr/local/sbin/lf-portcheck for later updates, reads the merged config and refuses to start anything if a port is still public. That matters because a Docker port given no address listens on all of them, 0.0.0.0 and [::] alike, and IPv4 traffic to it skips ufw. Ports on 127.0.0.1 stay local from Docker Engine 28 onward, and the block won't continue on an older engine. After two or three minutes, cd /srv/langfuse && docker compose logs -f langfuse-web should say Ready.

Your account first, then the public address

There's no built-in admin login, and anyone can press Sign up. So sign up through a private tunnel, switch sign-up off, then open it to the world.

  1. On your laptop, run ssh -L 3000:127.0.0.1:3000 -N you@server-ip and open http://localhost:3000. Sign up, set up an organisation with one project, then copy the public key and secret key from the project settings.
  2. Create an A record pointing your chosen hostname at the server, put the hostname in D= on the first line below, and paste the block as root.
D=langfuse.example.com
cd /srv/langfuse && ! grep -q '^AUTH_DISABLE_SIGNUP=' .env &&
printf 'AUTH_DISABLE_SIGNUP=true\nNEXTAUTH_URL=https://%s\n' "$D" >> .env &&
docker compose up -d &&
[ "$(docker compose exec -T langfuse-web printenv AUTH_DISABLE_SIGNUP)" = true ] &&
apt-get install -y caddy &&
printf '%s {\n\treverse_proxy 127.0.0.1:3000\n}\n' "$D" > /etc/caddy/Caddyfile &&
caddy validate --config /etc/caddy/Caddyfile &&
ufw allow 80,443/tcp && systemctl reload caddy

It turns sign-up off, sets NEXTAUTH_URL to the HTTPS address people will type, restarts Langfuse and confirms sign-up really is off. Only then does it install Caddy (packaged in Ubuntu 24.04), write the site and open ports 80 and 443. Caddy runs outside Docker, and ufw keeps both its IPv4 and IPv6 ports shut until you add that rule.

With sign-up off, nobody new gets in, as the authentication guide warns: invited colleagues and first-time SSO sign-ins included. Either switch it back on briefly when someone joins, or keep sign-up on and make SSO the only door: set AUTH_DISABLE_USERNAME_PASSWORD=true and one provider that only your people can pass, such as Google limited by AUTH_GOOGLE_ALLOWED_DOMAINS. Those variables are not in the stock Compose file, so add them under langfuse-web in the override file; your first password account then needs the provider's account-linking setting described in the same guide. To trace images or audio, give MinIO a public name of its own in Caddy, then point LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT at it, as the blob storage guide explains.

Free core, paid extras

Langfuse's LICENSE file puts the code under MIT, except the top-level ee/ folder and the ee folders inside web/src, worker/src and packages/shared/src/server, which follow the separate ee/LICENSE. According to the licence-key page, every core feature and API works without limits. A paid key, set as LANGFUSE_EE_LICENSE_KEY on web and worker, unlocks audit logs, roles per project, retention policies, protected prompt labels, server-side masking, UI customisation, organisation-creator limits, SCIM and two admin APIs.

In our general reading, which isn't legal advice: MIT is a permissive licence, so if you hand copies to others, changed or not, the copyright line and the licence text must travel with them, and you owe nobody your source code. The enterprise folders may be copied and changed for development and testing, no subscription needed; using them otherwise needs a valid agreement with Langfuse.

Keeping your traces safe

Langfuse says its Compose setup has no backups of its own, so here are two scripts. lf-backup.sh stops the stack, packs the four data volumes (ClickHouse, PostgreSQL, Redis, MinIO) plus .env and the override file into a dated archive, checks it, starts everything again, and only then clears archives older than ten days (not with LF_NO_PRUNE=1). The restart is queued ahead of the stop, so Langfuse comes back even if the copy fails. lf-restore.sh checks the archive, backs up the current state with pruning off so your archive can't be cleared, checks again, refuses if .previous copies exist, moves current data and config aside, unpacks and starts.

install -d -m 700 /var/backups/langfuse
cat > /usr/local/sbin/lf-backup.sh <<'EOF'
set -euo pipefail; umask 077; cd /srv/langfuse
f=/var/backups/langfuse/langfuse-$(date +%F-%H%M%S).tar.gz
trap 'docker compose up -d' EXIT; docker compose stop
tar -czf "$f" -C /var/lib/docker/volumes langfuse_langfuse_{postgres,clickhouse,minio,redis}_data \
  -C /srv/langfuse .env docker-compose.override.yml
tar -tzf "$f" > /dev/null
docker compose up -d; trap - EXIT; echo "ok $f"
[ "${LF_NO_PRUNE:-0}" = 1 ] || find /var/backups/langfuse -name 'langfuse-*.tar.gz' -mtime +10 -delete
EOF
cat > /usr/local/sbin/lf-restore.sh <<'EOF'
set -euo pipefail; A="$(realpath -e "${1:?archive}")"; V=/var/lib/docker/volumes; L=/srv/langfuse
tar -tzf "$A" > /dev/null
LF_NO_PRUNE=1 bash /usr/local/sbin/lf-backup.sh
P="$(echo $V/langfuse_langfuse_{postgres,clickhouse,minio,redis}_data) $L/.env $L/docker-compose.override.yml"
for p in $P; do [ ! -e "$p.previous" ] || { echo "$p.previous exists"; exit 1; }; done
tar -tzf "$A" > /dev/null; cd $L; docker compose down
for p in $P; do mv "$p" "$p.previous"; done
tar -xzpf "$A" -C $V langfuse_langfuse_{postgres,clickhouse,minio,redis}_data
tar -xzpf "$A" -C $L .env docker-compose.override.yml
docker compose up -d; echo restored
EOF
printf '15 4 * * * root bash /usr/local/sbin/lf-backup.sh > /var/log/lf-backup.log 2>&1\n' > /etc/cron.d/lf-backup

Restore as root with bash /usr/local/sbin/lf-restore.sh /var/backups/langfuse/langfuse-2026-10-30-041500.tar.gz, look over your traces, then delete the .previous copies. If it looks wrong, docker compose down in /srv/langfuse, delete the restored folders and files, give the .previous copies their old names and start again. The nightly run means a few minutes offline at 04:15, so traces sent then may not arrive.

Push each archive to another machine using rsync -a (leave out --delete), and tidy that side up separately; a backup on the same drive goes with it. The PostgreSQL volume only restores into the same major version it came from, 17 in today's file.

Moving to a fresh server after a failure? lf-restore.sh needs a running install to back up, so do it by hand there: set up Docker and the firewall, clone Langfuse into /srv/langfuse without starting it, copy the archive across, unpack the four volumes into /var/lib/docker/volumes and the two config files into /srv/langfuse with the same tar -xzpf lines the script uses, and start it with docker compose up -d from that folder.

Updating, and when one server isn't enough

Within version 4, the :4 tags follow the newest release and migrations run at start-up. Take a backup, then run cd /srv/langfuse && git pull --ff-only && bash /usr/local/sbin/lf-portcheck && docker compose pull --quiet && docker compose up -d. The repository pull matters because ClickHouse is pinned in the file, and a release that opened a new port stops at the check instead of going live. Moving between major versions has its own guide; version 4 wants ClickHouse 25.12 or later and PostgreSQL 15 or later.

Langfuse's own deployment table puts Docker Compose in the local use and testing row, and its Compose page admits there is no high availability or scaling; a bigger single server is how it grows. If a server failure must not stop your traces, its documented route is spreading Kubernetes over several servers; Kubernetes managed by us runs on whole machines, ordered in New York. For more on the trace store itself, read self-hosted ClickHouse, and to keep an eye on the server, Prometheus and Grafana.

Questions people ask

Is self-hosted Langfuse really free?
The core is MIT-licensed and the licence-key page says core features and APIs have no limits without a key. You pay only if you want the enterprise extras, such as audit logs, retention policies or project-level roles. You can drop a key later and the core carries on working.
Will my Langfuse server talk to the outside world?
A little, and you can see where. The free edition's telemetry switches off with TELEMETRY_ENABLED=false, which the setup above sets. The web container's release check to langfuse.com is not configurable; block it with an outbound firewall if you like, and it fails quietly. Your team's browsers also load a few images and videos from Langfuse hosts, and LANGFUSE_DISABLE_SIGNUP_ONBOARDING=true skips the newsletter step after sign-up. The images themselves come through docker.langfuse.com, a pull-counting proxy for Docker Hub.
Can I make old traces expire automatically?
Retention policies need an enterprise key. On the free edition your traces stay until you delete them, so pick a drive with room for the history you want and keep an eye on disk space.

Give your AI app's traces a home of their own

Pick a Xeon D-1500 cartridge, mirror its NVMe drives, and choose memory to match the trace history you plan to keep. No contract, and nothing meters the port.

Get a server for your Langfuse

Read next