Déployer Langfuse avec Docker Compose
La DSI d'Arvea a posé deux conditions : les conversations des conseillers ne quittent pas l'infrastructure de la mutuelle, et l'outil d'observabilité doit pouvoir être reconstruit de zéro par une seule commande. Langfuse est open source (licence MIT pour le cœur) et se déploie avec le fichier Compose que publie l'éditeur. Le travail consiste à le démarrer sans en hériter les mots de passe par défaut ni les ports inutiles.
Le mécanisme : six conteneurs, deux chemins
Langfuse v4 n'est pas un conteneur unique. Le fichier officiel en démarre six, et chacun a un rôle précis :
| Service | Rôle | Ce qui y est stocké |
|---|---|---|
langfuse-web | interface, API publique, point d'entrée OpenTelemetry (OTLP) | rien |
langfuse-worker | traite les files d'attente : ingestion, calcul des coûts, évaluations | rien |
| PostgreSQL 17 | données transactionnelles | comptes, projets, clés, prompts, jeux de données, prix des modèles |
| ClickHouse 25.12 | base analytique en colonnes | observations (spans, générations), scores |
| Redis 7 | files d'attente et cache | éphémère, mais noeviction : il ne doit jamais jeter un message |
| MinIO | stockage objet compatible S3 | événements bruts reçus, pièces jointes (médias), exports |
sequenceDiagram participant SDK as "SDK Python" participant W as "langfuse-web" participant S3 as "MinIO (S3)" participant R as "Redis" participant K as "langfuse-worker" participant CH as "ClickHouse" SDK->>W: "POST /api/public/otel (lot de spans)" W->>S3: "écrit l'événement brut" W->>R: "met en file une référence" W-->>SDK: "accusé de réception" K->>R: "dépile" K->>S3: "relit l'événement" K->>K: "calcule usage et coût (prix des modèles)" K->>CH: "insère l'observation"
L'ingestion est asynchrone : l'API répond avant que la donnée soit lisible. Une trace apparaît dans l'interface quelques secondes après l'appel. C'est pour cela que le test de bout en bout de ce projet interroge l'API en boucle plutôt qu'une seule fois.
La mise en œuvre
1. Garder le fichier officiel intact
docker-compose.yml est la copie du fichier de github.com/langfuse/langfuse au 2026-10-15. Une seule modification : les images Langfuse sont épinglées en 4.59.0 au lieu de :4. Avec :4, un docker compose pull un lundi matin peut changer la version en production sans que personne ne l'ait décidé.
Les adaptations du lab vivent dans un second fichier, docker-compose.lab.yml. Quand Langfuse publie une nouvelle version du fichier officiel, vous le remplacez sans fusion manuelle.
services:
# Le fichier officiel publie PostgreSQL, ClickHouse, Redis et le worker sur 127.0.0.1.
# Inutile pour le lab, et 5432 / 6379 sont souvent déjà pris sur un poste de développeur.
langfuse-worker:
ports: !reset []
clickhouse:
ports: !reset []
redis:
ports: !reset []
postgres:
ports: !reset []
minio:
ports: !reset
- 9090:9000!reset (Compose 2.24 et plus) remplace la liste héritée au lieu de la compléter. Sans lui, une surcharge ports: s'ajoute aux ports du fichier de base.
2. Des secrets générés, pas recopiés
Le fichier officiel contient des valeurs par défaut marquées # CHANGEME (mysalt, miniosecret, une clé de chiffrement faite de zéros). Toutes sont lues via ${VARIABLE:-défaut} : il suffit de les définir dans .env, que Compose lit automatiquement. make setup copie .env.example et remplace chaque changeme par openssl rand -hex 32 :
SECRETS := NEXTAUTH_SECRET SALT ENCRYPTION_KEY POSTGRES_PASSWORD CLICKHOUSE_PASSWORD REDIS_AUTH MINIO_ROOT_PASSWORD LANGFUSE_INIT_USER_PASSWORD
setup:
@if [ ! -f .env ]; then cp .env.example .env; \
for k in $(SECRETS); do sed -i "s|^$$k=changeme|$$k=$$(openssl rand -hex 32)|" .env; done; \
...Les variables qui dépendent d'un mot de passe le référencent au lieu de le dupliquer : DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres, et les trois clés S3 valent ${MINIO_ROOT_PASSWORD}. Un mot de passe changé à un endroit ne casse pas les autres.
3. Une instance prête sans clic
Par défaut, Langfuse démarre vide : il faut créer un compte, une organisation, un projet, puis copier les clés d'API. Les variables LANGFUSE_INIT_* font tout cela au premier démarrage (initialisation dite headless) :
LANGFUSE_INIT_ORG_ID=arvea
LANGFUSE_INIT_PROJECT_ID=assistant-conseillers
LANGFUSE_INIT_PROJECT_RETENTION=30
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-changeme # remplacée par make setup
LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-changeme # remplacée par make setup
LANGFUSE_INIT_USER_EMAIL=admin@arvea.example
# ---- Client Langfuse (SDK Python) : après les clés INIT, qu'il référence ----
LANGFUSE_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
LANGFUSE_SECRET_KEY=${LANGFUSE_INIT_PROJECT_SECRET_KEY}Le même .env sert au serveur et à l'application : les clés du SDK sont, par construction, celles du projet créé. LANGFUSE_INIT_PROJECT_RETENTION=30 fixe une rétention de 30 jours, un choix RGPD que la leçon 6 détaille.
4. Démarrer et attendre la bonne chose
make setup # .venv, dépendances, .env avec secrets aléatoires
make up # docker compose -p lab-langfuse ... up -d, puis attente de /api/public/healthmake up interroge /api/public/health jusqu'à obtenir une réponse. Les conteneurs « Started » ne suffisent pas : au premier démarrage, langfuse-web applique les migrations PostgreSQL et ClickHouse avant de répondre.
Résultat obtenu
Exécution du 2026-10-15, sur un poste Linux de 32 Go :
$ make up
Container lab-langfuse-postgres-1 Healthy
Container lab-langfuse-clickhouse-1 Healthy
Container lab-langfuse-minio-1 Healthy
Container lab-langfuse-langfuse-web-1 Started
Container lab-langfuse-langfuse-worker-1 Started
Attente de Langfuse sur http://localhost:3000...... : prêt.
$ curl -s localhost:3000/api/public/health
{"status":"OK","version":"4.59.0"}
$ docker compose -p lab-langfuse -f docker-compose.yml -f docker-compose.lab.yml ps
SERVICE STATUS PORTS
clickhouse Up 5 minutes (healthy) 8123/tcp, 9000/tcp, 9009/tcp
langfuse-web Up 5 minutes 0.0.0.0:3000->3000/tcp
langfuse-worker Up 5 minutes 3030/tcp
minio Up 5 minutes (healthy)
postgres Up 5 minutes (healthy) 5432/tcp
redis Up 5 minutes (healthy) 6379/tcpSeul le port 3000 (et 9090 pour MinIO) est publié sur l'hôte. Mémoire mesurée après le test complet : web 1,0 Gio, worker 785 Mio, ClickHouse 553 Mio, PostgreSQL 123 Mio, MinIO 80 Mio, Redis 7 Mio, soit 2,6 Gio.
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
Bind for 127.0.0.1:6379 failed: port is already allocated | le fichier officiel publie Redis et PostgreSQL ; un autre projet local les utilise déjà (cas réel sur le poste de test) | ne publier que ce qui doit l'être (!reset) |
Le SDK répond Invalid authorization header alors que les clés sont « bonnes » | dans .env, LANGFUSE_PUBLIC_KEY=${LANGFUSE_INIT_PROJECT_PUBLIC_KEY} placé avant la ligne qu'il référence : l'interpolation lit une variable encore vide (constaté pendant la rédaction) | déclarer une variable avant de la référencer ; tester auth_check() au démarrage |
| Les identifiants des connexions LLM deviennent illisibles après une restauration | ENCRYPTION_KEY régénérée : Langfuse chiffre les clés de fournisseurs avec elle | sauvegarder .env avec les volumes, dans un coffre de secrets |
| Les liens de connexion renvoient vers le mauvais port | port de langfuse-web changé sans changer NEXTAUTH_URL | les deux bougent ensemble |
| Le disque se remplit sans que les données grossissent | journaux Docker json-file sans rotation | max-size / max-file dans daemon.json |
Tout a disparu après make down | down -v supprime les volumes : c'est voulu dans un lab | en production, sauvegardes PostgreSQL et ClickHouse planifiées, jamais -v |
La règle de décision
| Situation | Choix |
|---|---|
| Données sensibles, exigence de localisation, équipe capable d'opérer des bases | auto-hébergé (ce projet) |
| Pas de contrainte de localisation, équipe sans astreinte infra | Langfuse Cloud (région UE disponible) : mêmes SDK, zéro exploitation |
| Pilote de quelques semaines | Compose sur une VM, comme ici |
| Plus de quelques millions d'observations par jour, ou besoin de haute disponibilité | Helm sur Kubernetes, bases managées |
Surveillez dès le premier jour : la taille des volumes ClickHouse et MinIO, la longueur des files Redis (un worker arrêté les fait grossir), et la version des images.
Pratique
Après une mise à jour de Docker, un collègue relance la pile d'Arvea. Les conteneurs sont tous « Started » mais l'application reçoit des erreurs de connexion à Langfuse pendant une minute. Que faites-vous ?
Au démarrage, langfuse-web applique les migrations avant d'accepter des requêtes : « Started » ne veut pas dire « prêt ». Un sleep fixe est trop court sur une petite VM et trop long ailleurs. Supprimer les volumes efface toutes les traces pour un problème qui n'en est pas un.
La RSSI demande qu'aucune base de données de Langfuse ne soit joignable depuis le réseau du poste, même en local. Quelle modification est la bonne ?
La surcharge produit le même résultat sans diverger du fichier officiel : la prochaine mise à jour se fait par simple remplacement. Éditer le fichier officiel fonctionne mais oblige à refaire la modification à chaque version. Le pare-feu protège, mais laisser publier un port inutile reste une surface d'attaque.
Exercice. Arvea veut deux instances sur la même VM : lab-langfuse pour les tests et lab-langfuse-recette pour la recette, sur le port 3001. Quelles variables changez-vous et quelle commande lance la seconde ?
Voir la solution
Un second fichier d'environnement, par exemple .env.recette, avec des secrets distincts et :
NEXTAUTH_URL=http://localhost:3001
LANGFUSE_BASE_URL=http://localhost:3001Puis une surcharge supplémentaire qui change le port publié (ports: !reset ["3001:3000"] sur langfuse-web) et MinIO (9091:9000, avec LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://localhost:9091). Le nom de projet Compose sépare réseaux et volumes :
docker compose -p lab-langfuse-recette --env-file .env.recette \
-f docker-compose.yml -f docker-compose.lab.yml -f docker-compose.recette.yml up -dSans -p distinct, la seconde commande recréerait les conteneurs de la première.
- Je sais dire ce que stocke chacun des six services, et lequel perdre coûte le plus cher.
- Je sais surcharger le fichier officiel sans le modifier.
- Je sais générer les secrets et créer organisation, projet et clés sans passer par l'interface.
- Je sais vérifier qu'une instance est prête, pas seulement démarrée.