−25%

on annual Windows plans, until 31 Oct. See plans

EQVPS
Get started

VPS for Langfuse: see what your AI agents did

Self-host Langfuse to trace every LLM call and tool step your unattended agents make. The Docker Compose setup we tested, real RAM numbers and a plan that fits.

An agent that works at 3 a.m. with nobody watching is only as good as your ability to reconstruct what it did. "It burned $38 of tokens overnight" is a bad morning. "It burned $38 because a search tool returned an empty page and the planner retried it 400 times" is a bug you can fix by lunch. The second sentence needs traces.

Langfuse records every LLM call, prompt, tool step, token count and latency as a tree you can click through. Running it yourself keeps those prompts and responses — which tend to contain customer data, API output and the occasional secret — on a server you control instead of someone else's dashboard.

What it costs to run

Langfuse v4 isn't one small binary. It's six containers: the web app, a background worker, Postgres, ClickHouse for the events, Redis and MinIO for blobs. We installed it on a test server and measured about 3.5 GB of RAM in use with an empty project. ClickHouse is the part that keeps growing, because every span you send ends up there.

WorkloadPlan
A handful of agents, weeks of tracesMedium-IP — 6 vCPU, 6 GB, 45 GB ($20/mo)
An agent fleet, months of historyPro plans, 32 GB of RAM and up — on request

Smaller plans will install it and then struggle the moment ClickHouse and the worker want memory at the same time. I wouldn't go below 6 GB.

Where do the agents live? If they run on other machines, pick a plan with its own IPv4: they need an HTTPS endpoint, and on a NAT plan the outbound address isn't your own and only the SSH port is reachable from outside. If the agents run on this same server, Langfuse never has to face the internet — Medium on NAT ($12/mo) is enough for a trial. There are two locations, Germany and Finland, and availability is shown at checkout; choose the one closer to the agents, not to you. Servers are paid in crypto, no KYC.

Install

On a fresh Ubuntu 24.04 or Debian 12 server, as root. Docker first:

curl -fsSL https://get.docker.com | sh

Then Langfuse, with real secrets instead of the defaults baked into the compose file:

git clone --depth 1 https://github.com/langfuse/langfuse.git
cd langfuse

PG=$(openssl rand -hex 16); MINIO=$(openssl rand -hex 16)
cat > .env <<EOF
NEXTAUTH_URL=https://langfuse.example.com
NEXTAUTH_SECRET=$(openssl rand -hex 32)
SALT=$(openssl rand -hex 32)
ENCRYPTION_KEY=$(openssl rand -hex 32)
POSTGRES_PASSWORD=$PG
DATABASE_URL=postgresql://postgres:$PG@postgres:5432/postgres
CLICKHOUSE_PASSWORD=$(openssl rand -hex 16)
MINIO_ROOT_PASSWORD=$MINIO
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=$MINIO
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=$MINIO
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=$MINIO
REDIS_AUTH=$(openssl rand -hex 16)
TELEMETRY_ENABLED=false
EOF
chmod 600 .env

Two of those passwords have partners: the Postgres password also sits inside DATABASE_URL, and the MinIO password has to match the three S3_..._SECRET_ACCESS_KEY lines. That's why each comes from one variable.

Now the trap. The compose file publishes the web app on port 3000 and MinIO on 9090 to every interface, and ports published by Docker go around ufw. Bind both to localhost so the reverse proxy is the only way in:

sed -i 's/- 3000:3000/- 127.0.0.1:3000:3000/; s/- 9090:9000/- 127.0.0.1:9090:9000/' docker-compose.yml
docker compose up -d
curl -s http://127.0.0.1:3000/api/public/health

The last command should print {"status":"OK", ...}. The first start runs database migrations; on our test box the health check passed about 15 seconds after the containers came up.

HTTPS in front

Point langfuse.example.com at the server's IP, then:

apt install -y caddy
cat > /etc/caddy/Caddyfile <<'EOF'
langfuse.example.com {
    reverse_proxy 127.0.0.1:3000
}
EOF
systemctl reload caddy
ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw enable

Caddy fetches the certificate on its own. Open the site, create your account (it becomes the first organisation), add a project and copy its public and secret keys.

Then close the door behind you. Langfuse understands AUTH_DISABLE_SIGNUP, but the stock compose file doesn't hand it to the web container, so it takes one extra line under langfuse-web:

sed -i 's|^      NEXTAUTH_SECRET: \(.*\)$|      NEXTAUTH_SECRET: \1\n      AUTH_DISABLE_SIGNUP: ${AUTH_DISABLE_SIGNUP:-false}|' docker-compose.yml
echo "AUTH_DISABLE_SIGNUP=true" >> .env
docker compose up -d

From now on a sign-up attempt gets „Sign up is disabled“. Add teammates from inside the project instead.

Send traces from an agent

The Python SDK wraps your functions, and nested calls become nested spans:

python3 -m venv venv && . venv/bin/activate
pip install langfuse
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_BASE_URL=https://langfuse.example.com
from langfuse import get_client, observe

langfuse = get_client()  # reads the LANGFUSE_* variables

@observe(name="fetch-prices")
def fetch_prices(pair: str) -> dict:
    return {"pair": pair, "bid": 64210.5}

@observe(name="nightly-agent-run")
def run() -> str:
    data = fetch_prices("BTC/USDT")
    return f"done: {data['pair']}"

print(run())
langfuse.flush()  # short scripts exit before the background sender finishes

Don't skip LANGFUSE_BASE_URL. Without it the SDK quietly sends everything to Langfuse's own cloud, your keys don't exist there, and you end up staring at an empty dashboard.

To check from the shell that the spans arrived:

curl -s -u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" \
  "$LANGFUSE_BASE_URL/api/public/v2/observations?limit=5"

In v4 the older /api/public/traces endpoint answers with an error about events-only mode. That's expected, not a broken install. In a real agent you wrap the LLM call and each tool the same way; Langfuse also ships integrations for the popular LLM SDKs and agent frameworks, so often it's an import change rather than decorators everywhere.

What to keep an eye on

  • Disk. ClickHouse and MinIO keep everything you send. docker system df -v shows where the space went. If you trace long prompts and responses, 45 GB on Medium-IP fills up in weeks, not years. Check it monthly; when you outgrow it, a Pro plan with 400 GB and more is available on request.
  • Updates. The compose file pins the major version (:4). docker compose pull && docker compose up -d brings in fixes within it. Read the changelog before you change the major version: v4 is exactly where the traces endpoint went away.
  • Backups. What matters lives in Docker volumes: Postgres holds users, projects and keys, ClickHouse and MinIO hold the traces. Copy them off the server on a schedule.

Is it worth $20 a month? If your agents spend more than that on tokens — and most unattended agents do — knowing where the tokens went pays for itself the first time a loop runs away. One small agent that runs once a day doesn't need any of this; a log file and a token counter will do.

Ready to deploy? Pay with crypto, no KYC — live in about a minute.

Deploy now →

FAQ

How much RAM does self-hosted Langfuse need?

On our test server Langfuse v4 ran six containers — web, worker, Postgres, ClickHouse, Redis and MinIO — and used about 3.5 GB of RAM with an empty project. ClickHouse grows with every span you send. Medium-IP (6 GB of RAM, 45 GB of disk) runs it comfortably for a handful of agents; a whole agent fleet with months of history needs a Pro plan, available on request.

Do I need a dedicated IP for Langfuse?

Only if your agents run somewhere else and send traces over the internet: they need an HTTPS endpoint, and a NAT plan only accepts your SSH port from outside. If the agents live on the same server, Langfuse can stay on localhost and you open the dashboard through an SSH tunnel — the Medium plan on NAT is fine for that.

My code runs, but the dashboard stays empty. Why?

Almost always a missing LANGFUSE_BASE_URL. Without it the SDK sends to Langfuse's hosted cloud, your self-hosted keys don't exist there, and nothing shows up on your server. Set the variable to your own address.

The /api/public/traces endpoint returns an error. Is the install broken?

No. Langfuse v4 runs in an events-only mode where that endpoint is switched off. Read data through /api/public/v2/observations instead — that's the call we used to confirm our test spans had arrived.

How do I stop strangers from signing up on my instance?

Langfuse reads AUTH_DISABLE_SIGNUP, but the stock docker-compose.yml doesn't pass it to the web container, so adding it to .env alone does nothing. Add one line to the langfuse-web environment (the command is below), set it to true after you've created your own account, and further sign-ups get „Sign up is disabled“.

Comments

No comments yet. Be the first.

Leave a comment

Comments are moderated before they appear.