Guide~15 min · intermediate

Deploy Langfuse on AWS EC2

The full v3 stack with Docker Compose — Postgres, ClickHouse, Redis and MinIO — behind an HTTPS reverse proxy. From an empty instance to your first traced project.

StackEC2DockerPostgres 16ClickHouseRedis 7MinIOCaddy
01

The architecture, in 6 services

Since v3, Langfuse is no longer a single container + Postgres. Traces now live in ClickHouse (a columnar OLAP database), the queue in Redis, and raw payloads in S3 storage — here, MinIO. Six services in total.

Careful

ClickHouse is required since v3. If it's missing, the web container crash-loops (it can't reach clickhouse:9000). We set it up in step 04.

// ingestion flow
SDK
client
web
ui + api
worker
jobs
Postgres
state
ClickHouse
traces
Redis
queue
MinIO
s3
·web — serves the console UI and the API
·worker — processes events and background jobs
·Postgres — transactional state (users, projects, config)
·ClickHouse — traces, observations and scores
·Redis — the queue and cache
·MinIO — S3 storage for raw payloads
02

Prerequisites — the EC2 instance

Six containers, ClickHouse among them: be generous with RAM. Below 4 GB, startup fails silently.

Typet3.large — 2 vCPU / 8 GB (4 GB minimum)
Disque30 GB gp3 minimum
OSUbuntu 24.04 LTS
Archiamd64 — not Graviton / ARM
Careful

Avoid t4g / Graviton (ARM64) instances: the official images target amd64, and ARM causes crash loops. Stick to t3.

Security group — ports to open

22 / SSHyour IP only
80 / HTTPpublic — Caddy redirects to 443
443 / HTTPSpublic — Langfuse traffic
Worth noting

Never expose 3005, 8123 or 9000 publicly — those are internal ports. Also point a DNS A record for langfuse.mondomaine.com at the instance's elastic IP before the HTTPS step.

03

Install Docker

Once connected over SSH, install Docker and the Compose plugin via the official script.

shellBASHCopy
# Ubuntu 24.04 LTS sur ton EC2 (amd64)
$ sudo apt-get update
$ curl -fsSL https://get.docker.com | sudo sh # Docker + plugin compose
$ sudo usermod -aG docker $USER # docker sans sudo
$ newgrp docker
$ docker --version && docker compose version
Worth noting

Recent Docker uses docker compose (with a space). Your docker-compose (with a hyphen) commands also work if you install the old v1 binary — this guide uses the plugin.

04

The docker-compose.yml

Create the folder and the file. Here's the full compose: Postgres, ClickHouse, Redis and MinIO, with healthchecks and ordered startup.

~/langfuse/docker-compose.ymlYAMLCopy
services:
db:
image: postgres:16
restart: always
environment:
POSTGRES_USER: langfuse
POSTGRES_PASSWORD: langfuse
POSTGRES_DB: langfuse
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U langfuse"]
interval: 5s
timeout: 5s
retries: 10
clickhouse: # indispensable depuis la v3
image: clickhouse/clickhouse-server
restart: always
environment:
CLICKHOUSE_USER: clickhouse
CLICKHOUSE_PASSWORD: clickhouse
volumes:
- chdata:/var/lib/clickhouse
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8123/ping"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: redis:7
restart: always
command: --requirepass myredissecret
volumes:
- redisdata:/data
minio:
image: minio/minio
restart: always
entrypoint: sh
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
environment:
MINIO_ROOT_USER: minio
MINIO_ROOT_PASSWORD: miniosecret
volumes:
- miniodata:/data
worker:
image: ghcr.io/langfuse/langfuse-worker:latest
restart: always
depends_on:
db: { condition: service_healthy }
clickhouse: { condition: service_healthy }
redis: { condition: service_started }
minio: { condition: service_started }
env_file:
- .env
web:
image: ghcr.io/langfuse/langfuse:latest
restart: always
depends_on:
db: { condition: service_healthy }
clickhouse: { condition: service_healthy }
redis: { condition: service_started }
minio: { condition: service_started }
worker: { condition: service_started }
ports:
- "3005:3000" # retire cette ligne une fois Caddy en place (etape 07)
env_file:
- .env
volumes:
pgdata:
chdata:
redisdata:
miniodata:
Good reflex

Key points: the clickhouse service and its chdata volume, healthchecks on db and clickhouse, and a conditional depends_on (condition: service_healthy) so web/worker wait for the databases to be ready.

05

The .env file

Both web and worker read this .env. Start by generating the three secrets.

Generate the secrets

shellBASHCopy
# genere un secret par ligne, colle-les dans le .env
openssl rand -base64 32 # -> NEXTAUTH_SECRET
openssl rand -base64 32 # -> SALT
openssl rand -hex 32 # -> ENCRYPTION_KEY (64 caracteres hex)

The full file

~/langfuse/.envENVCopy
# ---- Coeur ----
DATABASE_URL=postgresql://langfuse:langfuse@db:5432/langfuse
NEXTAUTH_URL=https://langfuse.mondomaine.com # l'URL HTTPS publique EXACTE
NEXTAUTH_SECRET=colle_ici_le_1er_openssl
SALT=colle_ici_le_2eme_openssl
ENCRYPTION_KEY=colle_ici_le_openssl_hex_32
TELEMETRY_ENABLED=true
# ---- ClickHouse ----
CLICKHOUSE_URL=http://clickhouse:8123
CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000
CLICKHOUSE_USER=clickhouse
CLICKHOUSE_PASSWORD=clickhouse
CLICKHOUSE_CLUSTER_ENABLED=false
# ---- Redis ----
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_AUTH=myredissecret
# ---- S3 / MinIO : upload des evenements ----
LANGFUSE_S3_EVENT_UPLOAD_ENABLED=true
LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_EVENT_UPLOAD_PREFIX=events/
# ---- S3 / MinIO : upload multimodal (images, fichiers) ----
LANGFUSE_S3_MEDIA_UPLOAD_ENABLED=true
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://minio:9000
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=minio
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=miniosecret
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX=media/
Careful

NEXTAUTH_URL must be the exact public HTTPS URL, otherwise login breaks (invalid OAuth redirects). And change the default passwords (minio, redis, clickhouse) before going to production.

06

Launch the stack

shellBASHCopy
$ mkdir -p ~/langfuse && cd ~/langfuse
# (colle ici docker-compose.yml et .env)
$ docker compose up -d
$ sleep 20
$ docker compose ps
$ echo "--- logs worker ---"
$ docker compose logs worker --tail=40
$ docker compose logs -f web # suis les migrations ClickHouse (1-2 min)

On first startup, the worker applies the ClickHouse migrations — this can take one to two minutes. Follow docker compose logs -f web until you see the server listening on port 3000.

Worth noting

web listens internally on 3000 (mapped to 3005). We don't expose it to the world — the reverse proxy handles that in the next step.

07

Reverse proxy + HTTPS (Caddy)

Caddy handles HTTPS automatically (Let's Encrypt certificate, renewal included) — it's the simplest option. We add it to the same compose so it can reach web internally.

~/langfuse/docker-compose.ymlYAMLCopy
# a ajouter dans la section services: du docker-compose.yml
caddy:
image: caddy:2
restart: always
depends_on:
- web
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddydata:/data
- caddyconfig:/config
# ... et dans volumes: ajoute
# caddydata:
# caddyconfig:
~/langfuse/CaddyfileTEXTCopy
langfuse.mondomaine.com {
reverse_proxy web:3000
}

Then remove the ports: - "3005:3000" mapping from the web service (Caddy reaches it internally via web:3000), and restart: docker compose up -d.

Good reflex

Caddy fetches the certificate on its own at the first hit on https://langfuse.mondomaine.com. If you prefer Nginx + certbot, the principle is identical: proxy to web:3000.

08

First login & project

Open your domain over HTTPS. The first account created is yours — then create an organization, a project, and grab the API keys.

shellBASHCopy
# 1. https://langfuse.mondomaine.com -> "Sign up" (1er compte)
# 2. Cree une organisation, puis un projet
# 3. Copie les cles API du projet :
# LANGFUSE_PUBLIC_KEY=pk-lf-...
# LANGFUSE_SECRET_KEY=sk-lf-...
# LANGFUSE_HOST=https://langfuse.mondomaine.com
$ pip install langfuse
$ python -c "from langfuse import Langfuse; Langfuse().auth_check()" # -> True
Worth noting

To close signups once your account exists, add AUTH_DISABLE_SIGNUP=true to the .env and rerun docker compose up -d.

09

Troubleshooting

The web container keeps restarting

Almost always an unreachable ClickHouse. Check that the hostname is clickhouse (not localhost) and that CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000. Containers reach each other by service name on the Docker network.

Check health

shellBASHCopy
# vivant ?
$ curl -s https://langfuse.mondomaine.com/api/public/health
# pret a servir du trafic ?
$ curl -s https://langfuse.mondomaine.com/api/public/ready
# 200 = ok · 503 = base injoignable

Other gotchas

·Timezone: Postgres AND ClickHouse must run in UTC, otherwise queries return empty results.
·ARM64: if you are on Graviton, switch back to an amd64 instance (t3).
·Page unreachable: check the security group (80/443 open) and the DNS record.
·Targeted logs: docker compose logs web --tail=50 -f (same for worker).
Careful

docker compose down -v deletes the volumes = total data loss. To just stop, use docker compose down without the -v.

10

Backups & restore

A running stack is good; a stack you can restore is better. The single-node trap is that data lives across three heterogeneous systems: transactional state in Postgres, traces in ClickHouse, raw payloads in MinIO. Losing any one of them breaks consistency. So you back up all three — not just Postgres out of reflex. The Langfuse backups doc covers the approach for each component.

Postgres — pg_dump

Simplest and most reliable: a compressed daily dump with rotation. Run it from the host, straight into the db container.

~/langfuse/backup-pg.shBASHCopy
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F_%H%M)
OUT=~/backups/pg
mkdir -p "$OUT"
docker compose exec -T db \
pg_dump -U postgres -Fc langfuse \
> "$OUT/langfuse_${STAMP}.dump"
# rotation : on garde 7 jours
find "$OUT" -name '*.dump' -mtime +7 -delete

ClickHouse — BACKUP … TO S3

ClickHouse has its own native backup command that writes straight to an S3 bucket. It's far cleaner than tarring the chdata volume (which would require the container stopped). Caveat: restore must target an identical or compatible ClickHouse version.

clickhouse-clientSQLCopy
-- sauvegarde complète vers S3
BACKUP DATABASE default
TO S3('https://s3.eu-west-3.amazonaws.com/mon-bucket/ch/{stamp}',
'ACCESS_KEY', 'SECRET_KEY');
-- restauration
RESTORE DATABASE default
FROM S3('https://s3.eu-west-3.amazonaws.com/mon-bucket/ch/{stamp}',
'ACCESS_KEY', 'SECRET_KEY');
Worth noting

If you stay on single-node Docker without external S3, the documented alternative is a volume snapshot (or a tar of chdata with the container stopped). The key point stands: don't mix a ClickHouse dump from one version with a binary from another major version.

MinIO — sync to S3

Raw payloads (ingestion events) land in MinIO first, before processing. Replicate them to a real S3 bucket with the mc client or aws s3 sync. In fact, for production the docs flatly recommend swapping MinIO for native S3 — one backup concern removed.

Careful

An untested backup is not a backup. Spin up a throwaway instance, restore all three systems onto it, and confirm an existing trace shows up in the UI. That's the only test that counts.

11

Sizing, scaling & hardening

The single-node from this guide holds up fine for a team at moderate volume. When it grows, two levers: scale the worker horizontally, and give ClickHouse more RAM. The scaling doc gives concrete thresholds.

Sizing reference points

web / worker2 CPU / 4 GB each — above sustained 50% CPU on the worker, add an instance
ClickHouse8 GB minimum, ≥ 16 GB for large volumes (scale vertically)
Postgres2 CPU / 4 GB
Redis / Valkey1 CPU / 1.5 GB — ≥ 4 CPU + cluster mode under heavy load
Worth noting

ClickHouse scales mostly vertically: more RAM before more nodes. And Langfuse does not support multi-shard clusters — shard count stays at 1, you add replicas, not shards.

Scaling the worker in Compose

On a single machine you can already run several worker replicas: they share the Redis queue. web stays behind the reverse proxy.

shellBASHCopy
# 3 workers, même file Redis
docker compose up -d --scale worker=3

Hardening — the non-negotiables

The encryption config is clear: LLM API keys and integration secrets stored in Langfuse are encrypted with ENCRYPTION_KEY, and Langfuse API keys are hashed with SALT. Both values must be generated, kept secret, and above all stable — lose ENCRYPTION_KEY and you lose access to everything it encrypted.

shellBASHCopy
# ENCRYPTION_KEY : 256 bits, 64 caractères hex
openssl rand -hex 32
# à mettre dans .env — et à sauvegarder ailleurs que sur l'instance
·ENCRYPTION_KEY: 64 hex chars (openssl rand -hex 32), stable and backed up off the instance.
·Encryption at rest: enable disk encryption (EBS) and, where possible, database-level encryption.
·Close signups after your account: AUTH_DISABLE_SIGNUP=true.
·Internal ports (3005, 8123, 9000, 9001) never exposed: only 443 leaves, via the proxy.

Upgrades

To stay within a major version, pin the langfuse/langfuse:3 tag (and the worker at :3): migrations apply automatically on startup. For a major jump (v2 → v3), follow the dedicated migration guide — it's not a plain pull.

Careful

Before any upgrade: back up Postgres AND ClickHouse. A schema migration that runs halfway on an unbacked database is the worst case. The ClickHouse schema is not a stable contract — it changes between versions.

12

Sources & further reading

Sources & further reading
01Langfuse — Backup Strategies (self-hosted)The reference for backing up Postgres, ClickHouse and blob storage, with example scripts.02Langfuse — Scaling DeploymentsSizing thresholds and scaling strategy (horizontal worker, vertical ClickHouse).03Langfuse — ClickHouse (self-hosted)ClickHouse config: S3-backed disks, single-node vs cluster, and the 'no multi-shard' rule.04Langfuse — Encryption (self-hosted)What's encrypted (ENCRYPTION_KEY) vs hashed (SALT), and encryption at rest.05Langfuse — Upgrade (self-hosted)Image tag strategy and automatic migrations on startup.06Langfuse — Self-hosting overviewThe entry point to the whole v3 self-hosting documentation.07Langfuse & ClickHouse: a new data stack for LLM appsWhy v3 moved to ClickHouse — the context behind the 6-service architecture.
In short

It's live. Next steps: wire a SDK into your agents, build cost dashboards, and set up backups (pg_dump for Postgres, snapshots for ClickHouse and MinIO).

Official Langfuse docs ↗
Read next