Lección 1 — Déployer

Déployer Langfuse avec Docker Compose

9 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

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 :

ServiceRôleCe qui y est stocké
langfuse-webinterface, API publique, point d'entrée OpenTelemetry (OTLP)rien
langfuse-workertraite les files d'attente : ingestion, calcul des coûts, évaluationsrien
PostgreSQL 17données transactionnellescomptes, projets, clés, prompts, jeux de données, prix des modèles
ClickHouse 25.12base analytique en colonnesobservations (spans, générations), scores
Redis 7files d'attente et cacheéphémère, mais noeviction : il ne doit jamais jeter un message
MinIOstockage 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.

files/docker-compose.lab.yml
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 :

files/Makefile (extrait)
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) :

files/.env.example (extrait)
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/health

make 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/tcp

Seul 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ômeCauseParade
Bind for 127.0.0.1:6379 failed: port is already allocatedle 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 restaurationENCRYPTION_KEY régénérée : Langfuse chiffre les clés de fournisseurs avec ellesauvegarder .env avec les volumes, dans un coffre de secrets
Les liens de connexion renvoient vers le mauvais portport de langfuse-web changé sans changer NEXTAUTH_URLles deux bougent ensemble
Le disque se remplit sans que les données grossissentjournaux Docker json-file sans rotationmax-size / max-file dans daemon.json
Tout a disparu après make downdown -v supprime les volumes : c'est voulu dans un laben production, sauvegardes PostgreSQL et ClickHouse planifiées, jamais -v

La règle de décision

SituationChoix
Données sensibles, exigence de localisation, équipe capable d'opérer des basesauto-hébergé (ce projet)
Pas de contrainte de localisation, équipe sans astreinte infraLangfuse Cloud (région UE disponible) : mêmes SDK, zéro exploitation
Pilote de quelques semainesCompose 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 ?

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 ?

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:3001

Puis 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 -d

Sans -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.
Siguiente