Back to resources
// guide · deployment

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.

GuideBy Julien~15 min · intermediate
EC2DockerPostgres 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.

!

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
!

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
i

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.

shellcopy
# 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
i

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.ymlcopy
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:

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

shellcopy
# 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/.envcopy
# ---- 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/
!

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

shellcopy
$ 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.

i

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.ymlcopy
# 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/Caddyfilecopy
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.

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.

shellcopy
# 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
i

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

shellcopy
# 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).
!

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

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).