Auto-héberger Langfuse sur un VPS pour l'observabilité LLM

13 min de lecture·Matthieu·llm-observabilityci-cddeepevaldocker-composelangfuse|

Déployez Langfuse v3 avec Docker Compose sur votre propre VPS. Tracez les appels LLM, suivez les coûts, lancez des évaluations automatisées avec DeepEval et intégrez des portes qualité dans votre pipeline CI/CD.

Langfuse est une plateforme open source d'observabilité LLM. Elle trace chaque appel LLM de votre application : latence, consommation de tokens, coût et paires prompt/complétion. L'auto-héberger avec Docker Compose garde toutes les données de trace sur votre infrastructure. Pas de facturation à l'événement. Pas de données qui quittent votre réseau.

Ce tutoriel couvre le cycle complet : déployer Langfuse v3, ajouter TLS, instrumenter des applications Python et TypeScript, et construire des pipelines d'évaluation automatisés avec DeepEval intégrés au CI/CD.

De quoi Langfuse v3 a-t-il besoin pour tourner sur un VPS ?

Langfuse v3 fait tourner six conteneurs : l'interface web, un worker asynchrone, PostgreSQL pour les métadonnées, ClickHouse pour l'analytique des traces, Redis pour la file d'attente et MinIO pour le stockage d'objets. C'est un changement important par rapport à la v2, qui n'avait besoin que de PostgreSQL.

Composant Rôle Port par défaut RAM de base
langfuse-web Interface web et API 3000 ~512 Mo
langfuse-worker Traitement asynchrone des événements 3030 ~512 Mo
PostgreSQL 17 Métadonnées transactionnelles 5432 ~256 Mo
ClickHouse Analytique OLAP des traces 8123 (HTTP), 9000 (natif) ~1 Go
Redis 7 File d'attente et cache 6379 ~128 Mo
MinIO Stockage d'objets/médias 9000 (API), 9001 (console) ~256 Mo

Prévoyez au minimum 4 vCPU et 8 Go de RAM. Un Virtua Cloud VCS-8 (4 vCPU, 8 Go RAM, NVMe) gère ça confortablement. Commencez avec 100 Go de disque. ClickHouse grossit d'environ 1 à 2 Go par million de traces selon la taille des prompts/complétions.

Dimensionnement par volume

Traces/mois Croissance disque/mois VPS recommandé
< 100K ~500 Mo 4 vCPU / 8 Go
100K - 1M 1-2 Go 4 vCPU / 8 Go
1M - 10M 10-20 Go 8 vCPU / 16 Go
> 10M 50+ Go Dédié / Kubernetes

Prérequis

Comment déployer Langfuse avec Docker Compose ?

Clonez le dépôt officiel et utilisez le docker-compose.yml fourni comme point de départ. L'étape clé consiste à générer de vrais secrets au lieu d'utiliser les valeurs par défaut.

mkdir -p /opt/langfuse && cd /opt/langfuse

Créez le fichier d'environnement avec des secrets générés :

cat > .env << 'ENVEOF'
# PostgreSQL
POSTGRES_USER=langfuse
POSTGRES_PASSWORD=REPLACE_PG
POSTGRES_DB=langfuse

# ClickHouse
CLICKHOUSE_USER=clickhouse
CLICKHOUSE_PASSWORD=REPLACE_CH

# MinIO
MINIO_ROOT_USER=minio
MINIO_ROOT_PASSWORD=REPLACE_MINIO

# Redis
REDIS_AUTH=REPLACE_REDIS

# Langfuse secrets
NEXTAUTH_SECRET=REPLACE_NEXTAUTH
SALT=REPLACE_SALT
ENCRYPTION_KEY=REPLACE_ENCRYPTION

# Langfuse config
NEXTAUTH_URL=https://langfuse.example.com
LANGFUSE_CSP_ENFORCE_HTTPS=true
KEEP_ALIVE_TIMEOUT=70
ENVEOF

Remplacez les valeurs temporaires par de vrais aléatoires. Utilisez openssl rand -hex plutôt que -base64 car la sortie base64 contient des caractères /, + et = qui cassent les URL de connexion PostgreSQL :

sed -i "s|REPLACE_PG|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_CH|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_MINIO|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_REDIS|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_NEXTAUTH|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_SALT|$(openssl rand -hex 32)|" .env
sed -i "s|REPLACE_ENCRYPTION|$(openssl rand -hex 32)|" .env

Verrouillez le fichier. Seul root doit pouvoir le lire :

chmod 600 .env
ls -la .env
-rw------- 1 root root  715 Mar 19 10:00 .env

Comment résoudre le conflit de port 9000 entre MinIO et ClickHouse ?

MinIO et ClickHouse utilisent tous les deux le port 9000 par défaut. Le docker-compose.yml officiel mappe déjà le port API de MinIO sur 9090 côté hôte (9090:9000), ce qui évite le conflit. Si vous écrivez un fichier compose personnalisé, pensez à remapper l'un des deux.

Le fichier compose officiel lie aussi les ports d'infrastructure à 127.0.0.1 uniquement (PostgreSQL, ClickHouse natif, Redis, console MinIO), empêchant tout accès externe. Le seul port exposé sur toutes les interfaces est le 3000 pour l'interface web, que nous allons placer derrière un reverse proxy.

Créez le docker-compose.yml :

services:
  langfuse-web:
    image: docker.io/langfuse/langfuse:3
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      - NEXTAUTH_URL=${NEXTAUTH_URL}
      - NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
      - SALT=${SALT}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000
      - CLICKHOUSE_URL=http://clickhouse:8123
      - CLICKHOUSE_USER=${CLICKHOUSE_USER}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
      - CLICKHOUSE_CLUSTER_ENABLED=false
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_AUTH=${REDIS_AUTH}
      - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
      - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
      - LANGFUSE_CSP_ENFORCE_HTTPS=${LANGFUSE_CSP_ENFORCE_HTTPS}
      - KEEP_ALIVE_TIMEOUT=${KEEP_ALIVE_TIMEOUT}
      - LANGFUSE_LOG_LEVEL=info
    depends_on:
      postgres:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
      minio:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  langfuse-worker:
    image: docker.io/langfuse/langfuse-worker:3
    ports:
      - "127.0.0.1:3030:3030"
    environment:
      - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
      - NEXTAUTH_URL=${NEXTAUTH_URL}
      - SALT=${SALT}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000
      - CLICKHOUSE_URL=http://clickhouse:8123
      - CLICKHOUSE_USER=${CLICKHOUSE_USER}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
      - CLICKHOUSE_CLUSTER_ENABLED=false
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_AUTH=${REDIS_AUTH}
      - LANGFUSE_S3_EVENT_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_EVENT_UPLOAD_REGION=auto
      - LANGFUSE_S3_MEDIA_UPLOAD_BUCKET=langfuse
      - LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT=http://minio:9000
      - LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID=${MINIO_ROOT_USER}
      - LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
      - LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE=true
      - LANGFUSE_S3_MEDIA_UPLOAD_REGION=auto
      - LANGFUSE_LOG_LEVEL=info
    depends_on:
      postgres:
        condition: service_healthy
      clickhouse:
        condition: service_healthy
      minio:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  postgres:
    image: docker.io/postgres:17
    ports:
      - "127.0.0.1:5432:5432"
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
    volumes:
      - langfuse_postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 3s
      timeout: 3s
      retries: 10
    restart: unless-stopped

  clickhouse:
    image: docker.io/clickhouse/clickhouse-server
    user: "101:101"
    ports:
      - "127.0.0.1:8123:8123"
      - "127.0.0.1:9000:9000"
    volumes:
      - langfuse_clickhouse_data:/var/lib/clickhouse
      - langfuse_clickhouse_logs:/var/log/clickhouse-server
    environment:
      - CLICKHOUSE_DB=default
      - CLICKHOUSE_USER=${CLICKHOUSE_USER}
      - CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD}
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
      interval: 5s
      timeout: 5s
      retries: 10
    restart: unless-stopped

  minio:
    image: docker.io/minio/minio
    entrypoint: sh
    command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
    ports:
      - "127.0.0.1:9090:9000"
      - "127.0.0.1:9091:9001"
    environment:
      - MINIO_ROOT_USER=${MINIO_ROOT_USER}
      - MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
    volumes:
      - langfuse_minio_data:/data
    healthcheck:
      test: ["CMD", "mc", "ready", "local"]
      interval: 3s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: docker.io/redis:7
    command: redis-server --requirepass ${REDIS_AUTH} --maxmemory-policy noeviction
    ports:
      - "127.0.0.1:6379:6379"
    volumes:
      - langfuse_redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_AUTH}", "ping"]
      interval: 3s
      timeout: 3s
      retries: 10
    restart: unless-stopped

volumes:
  langfuse_postgres_data:
  langfuse_clickhouse_data:
  langfuse_clickhouse_logs:
  langfuse_minio_data:
  langfuse_redis_data:

Quelques points à noter dans ce fichier compose :

  • Le conteneur web écoute sur 127.0.0.1:3000 au lieu de 0.0.0.0:3000. Tout le trafic passe par le reverse proxy.
  • CLICKHOUSE_MIGRATION_URL utilise le protocole natif (clickhouse://) sur le port 9000, tandis que CLICKHOUSE_URL utilise HTTP sur le port 8123. Les deux sont nécessaires. Si CLICKHOUSE_MIGRATION_URL manque, le conteneur web plante au démarrage.
  • Redis utilise des variables séparées REDIS_HOST, REDIS_PORT et REDIS_AUTH au lieu d'une chaîne de connexion. C'est le format attendu par Langfuse v3.
  • L'entrypoint de MinIO crée le répertoire du bucket langfuse au premier démarrage (mkdir -p /data/langfuse). Sans ça, les uploads S3 échouent tant que le bucket n'existe pas.
  • ClickHouse tourne avec user: "101:101" pour correspondre à l'utilisateur clickhouse interne au conteneur.
  • Redis tourne avec --maxmemory-policy noeviction pour éviter la perte de données quand la mémoire est limitée. Langfuse s'appuie sur Redis pour sa file de travaux ; évincer des clés causerait une perte de données silencieuse.

Démarrez la pile :

docker compose up -d

Attendez environ 2-3 minutes que tous les conteneurs s'initialisent. ClickHouse et PostgreSQL exécutent les migrations au premier démarrage. Vérifiez l'état :

docker compose ps

Les six conteneurs doivent afficher Up. Les conteneurs d'infrastructure (postgres, clickhouse, minio, redis) affichent (healthy). Les conteneurs langfuse-web et langfuse-worker n'ont pas de healthcheck défini et affichent Up sans indicateur de santé. Si un conteneur redémarre en boucle, consultez ses logs :

docker compose logs langfuse-web --tail 50

Interrogez le point de santé pour confirmer que l'API est opérationnelle et que la connexion à la base de données fonctionne :

curl -s http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true | python3 -m json.tool
{
    "status": "OK",
    "version": "3.160.0"
}

Comment ajouter TLS à une instance Langfuse auto-hébergée ?

Placez Caddy devant Langfuse. Caddy gère automatiquement l'obtention des certificats TLS auprès de Let's Encrypt. Pas de tâche cron certbot, pas de renouvellement manuel.

Installez Caddy :

apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list
apt update && apt install -y caddy

Créez le Caddyfile :

cat > /etc/caddy/Caddyfile << 'EOF'
langfuse.example.com {
    header -Server
    reverse_proxy 127.0.0.1:3000 {
        transport http {
            keepalive 75s
            keepalive_idle_conns 10
        }
    }
}
EOF

Le keepalive 75s est réglé plus haut que le KEEP_ALIVE_TIMEOUT=70 de Langfuse pour éviter que le reverse proxy garde des connexions périmées. Ce décalage est la cause principale des erreurs 502/504 intermittentes que rencontrent beaucoup d'auto-hébergeurs. La directive header -Server supprime les informations de version des réponses.

systemctl enable --now caddy

enable fait survivre Caddy aux redémarrages. --now le démarre immédiatement.

systemctl status caddy
 caddy.service - Caddy
     Loaded: loaded (/usr/lib/systemd/system/caddy.service; enabled; preset: enabled)
     Active: active (running)

Depuis votre machine locale, testez le point d'accès TLS :

curl -I https://langfuse.example.com/api/public/health
HTTP/2 200
content-type: application/json

Configurer l'authentification et les clés API

Ouvrez https://langfuse.example.com dans votre navigateur. Créez le premier compte utilisateur. Il devient le compte administrateur.

Après connexion :

  1. Créez un nouveau projet (par exemple « production »)
  2. Allez dans Settings > API Keys
  3. Cliquez sur Create API Key
  4. Sauvegardez la Public Key et la Secret Key. Vous avez besoin des deux pour instrumenter vos applications.

La clé publique identifie votre projet. La clé secrète authentifie les écritures. Traitez la clé secrète comme un mot de passe de base de données.

Comment instrumenter une application Python pour envoyer des traces à Langfuse ?

Le SDK Python de Langfuse repose sur OpenTelemetry. Le décorateur @observe() crée automatiquement des traces et des spans pour les fonctions décorées. Les appels imbriqués produisent des spans imbriqués dans l'interface Langfuse.

Installez le SDK :

pip install langfuse openai

Définissez les variables d'environnement pointant vers votre instance auto-hébergée :

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_HOST="https://langfuse.example.com"
export OPENAI_API_KEY="sk-..."

Voici une application tracée :

from langfuse import observe, propagate_attributes, get_client
from langfuse.openai import openai  # patched OpenAI client

@observe()
def retrieve_context(query: str) -> str:
    # Simulating a retrieval step
    return "Paris is the capital of France, with a population of 2.1 million."

@observe()
def answer_question(query: str) -> str:
    context = retrieve_context(query)

    response = openai.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": f"Answer based on this context: {context}"},
            {"role": "user", "content": query},
        ],
    )
    return response.choices[0].message.content

@observe()
def run_pipeline(query: str) -> str:
    with propagate_attributes(
        user_id="user_42",
        session_id="session_abc",
        tags=["production", "rag-pipeline"],
    ):
        return answer_question(query)

result = run_pipeline("What is the capital of France?")
print(result)

# Flush traces before exit in short-lived scripts
get_client().shutdown()

Le @observe() sur run_pipeline crée la trace racine. answer_question et retrieve_context deviennent des spans imbriqués. L'import patché d'openai (from langfuse.openai import openai) capture automatiquement le nom du modèle, le nombre de tokens, la latence et le coût sous forme de span de génération. Le gestionnaire de contexte propagate_attributes attache les métadonnées utilisateur et session à toutes les observations imbriquées.

Après avoir exécuté ce script, ouvrez le tableau de bord Langfuse. Le Trace Explorer affiche l'arbre d'appels complet avec le timing de chaque span.

Comment instrumenter une application TypeScript ?

Installez le SDK TypeScript de Langfuse et le SDK Node OpenTelemetry :

npm install @langfuse/tracing @langfuse/otel @opentelemetry/sdk-node

Définissez les mêmes variables d'environnement :

export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASEURL="https://langfuse.example.com"
import {
  observe,
  startActiveObservation,
  propagateAttributes,
} from "@langfuse/tracing";

const fetchContext = observe(
  async (query: string) => {
    return "Paris is the capital of France.";
  },
  { name: "fetch-context", asType: "span" }
);

const callLLM = observe(
  async (query: string, context: string) => {
    // Replace with your actual LLM call
    return `Based on context: ${context}, the answer is Paris.`;
  },
  { name: "llm-call", asType: "generation" }
);

await startActiveObservation("rag-pipeline", async (root) => {
  await propagateAttributes(
    { userId: "user_42", sessionId: "session_abc" },
    async () => {
      const context = await fetchContext("Capital of France?");
      const answer = await callLLM("Capital of France?", context);
      root.update({ output: { answer } });
    }
  );
});

Appelez forceFlush() sur le span processor avant la sortie des fonctions serverless ou des scripts pour éviter la perte de données.

Tour d'horizon du tableau de bord

Le tableau de bord Langfuse offre quatre vues principales :

Trace Explorer affiche les traces de requêtes individuelles. Cliquez sur une trace pour voir l'arbre complet des spans : quelles fonctions ont tourné, combien de temps chacune a pris et les prompts/complétions exacts envoyés au LLM. Filtrez par identifiant utilisateur, tags ou plage temporelle.

Suivi des coûts ventile les dépenses par modèle. Vous voyez quels modèles consomment le plus de tokens et quels appels API sont coûteux. Utilisez-le pour repérer les prompts qui gaspillent des tokens sur des complétions inutilement longues.

Percentiles de latence (p50, p90, p99) par endpoint ou nom de span. Si votre p99 grimpe, vous pouvez explorer les traces les plus lentes pour trouver le goulot d'étranglement.

Tendances d'utilisation des tokens dans le temps. Surveillez les sauts inattendus qui indiquent un changement de prompt ou un bug générant des sorties plus longues.

Comment mettre en place une évaluation LLM automatisée avec DeepEval et Langfuse ?

DeepEval est un framework open source d'évaluation LLM qui note les sorties de modèles sur des métriques comme l'hallucination, la fidélité et la pertinence. Combiné à Langfuse, vous pouvez récupérer les traces de production, lancer des évaluations dessus et renvoyer les scores au tableau de bord Langfuse.

Installez DeepEval :

pip install deepeval

Métriques d'évaluation

Métrique Ce qu'elle mesure Plage de score Quand l'utiliser
Hallucination Exactitude factuelle par rapport au contexte fourni 0-1 (1 = pas d'hallucination) Pipelines RAG
Faithfulness Alignement de la sortie avec le contexte de récupération 0-1 (1 = fidèle) Pipelines RAG
Answer Relevancy La réponse répond-elle à la question 0-1 (1 = pertinent) Toute application LLM
G-Eval Critères personnalisés via LLM-as-judge 0-1 Contrôles qualité personnalisés

Script d'évaluation

Ce script récupère les traces récentes de Langfuse, exécute les métriques DeepEval et renvoie les scores :

import os
from langfuse import Langfuse
from deepeval.metrics import GEval, AnswerRelevancyMetric
from deepeval.test_case import LLMTestCase, LLMTestCaseParams

langfuse = Langfuse(
    public_key=os.environ["LANGFUSE_PUBLIC_KEY"],
    secret_key=os.environ["LANGFUSE_SECRET_KEY"],
    host=os.environ["LANGFUSE_HOST"],
)

# Fetch traces tagged for evaluation
traces = langfuse.api.trace.list(
    tags=["eval-candidate"],
    limit=50,
).data

# Define metrics
relevancy = AnswerRelevancyMetric(threshold=0.7)
correctness = GEval(
    name="Correctness",
    criteria="Determine whether the output is factually correct based on the context.",
    evaluation_params=[
        LLMTestCaseParams.ACTUAL_OUTPUT,
        LLMTestCaseParams.EXPECTED_OUTPUT,
    ],
)

for trace in traces:
    test_case = LLMTestCase(
        input=trace.input,
        actual_output=trace.output,
        retrieval_context=[trace.metadata.get("context", "")],
    )

    relevancy.measure(test_case)
    correctness.measure(test_case)

    # Push scores back to Langfuse
    langfuse.create_score(
        trace_id=trace.id,
        name="relevancy",
        value=relevancy.score,
        comment=relevancy.reason,
    )
    langfuse.create_score(
        trace_id=trace.id,
        name="correctness",
        value=correctness.score,
        comment=correctness.reason,
    )

langfuse.shutdown()
print(f"Evaluated {len(traces)} traces")

Après exécution, les scores d'évaluation apparaissent dans le tableau de bord Langfuse aux côtés des traces originales. Vous pouvez filtrer les traces par score pour trouver les sorties de mauvaise qualité.

Comment lancer des évaluations LLM dans un pipeline CI/CD ?

Intégrez DeepEval dans GitHub Actions pour détecter les régressions de qualité avant qu'elles n'atteignent la production. Le workflow exécute votre suite d'évaluation sur un jeu de données de test à chaque pull request.

Créez .github/workflows/llm-eval.yml :

name: LLM Evaluation

on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'src/llm/**'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install deepeval langfuse openai

      - name: Run evaluation suite
        env:
          LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
          LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
          LANGFUSE_HOST: ${{ secrets.LANGFUSE_HOST }}
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: deepeval test run tests/test_llm_quality.py

      - name: Upload results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: eval-results
          path: .deepeval/

Créez le fichier de test tests/test_llm_quality.py :

import deepeval
from deepeval import assert_test
from deepeval.test_case import LLMTestCase
from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric
from deepeval.dataset import EvaluationDataset

# Load test cases from a JSON file or define inline
test_cases = [
    LLMTestCase(
        input="What is the capital of France?",
        actual_output=my_llm_function("What is the capital of France?"),
        retrieval_context=["France is a country in Western Europe. Its capital is Paris."],
    ),
    # Add more test cases covering your prompt changes
]

dataset = EvaluationDataset(test_cases=test_cases)

@deepeval.parametrize(dataset)
def test_answer_relevancy(test_case):
    metric = AnswerRelevancyMetric(threshold=0.7)
    assert_test(test_case, [metric])

@deepeval.parametrize(dataset)
def test_faithfulness(test_case):
    metric = FaithfulnessMetric(threshold=0.7)
    assert_test(test_case, [metric])

Le workflow ne se déclenche que quand les templates de prompts ou le code LLM changent. Si une métrique passe sous le seuil, le build de la PR échoue. Les développeurs voient quel cas de test a échoué et de combien dans les logs GitHub Actions.

Comment sauvegarder PostgreSQL et ClickHouse pour Langfuse ?

Auto-héberger sans sauvegardes est un risque. PostgreSQL contient les comptes utilisateurs, les paramètres de projet et les clés API. ClickHouse contient toutes les données de trace. MinIO contient les uploads médias et les exports par lots.

Sauvegarde PostgreSQL

docker compose exec postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +%F).sql.gz

Sauvegarde ClickHouse

Exportez les tables individuellement avec le client ClickHouse :

docker compose exec clickhouse clickhouse-client \
  --user clickhouse \
  --password "$(grep CLICKHOUSE_PASSWORD /opt/langfuse/.env | cut -d= -f2)" \
  --query "SELECT * FROM traces FORMAT Native" > /opt/backups/traces-$(date +%F).native

ClickHouse supporte aussi la commande BACKUP DATABASE pour les sauvegardes complètes, mais elle nécessite de configurer un allowed_disk dans la configuration du serveur ClickHouse. Pour les déploiements Docker, l'export par table ci-dessus est plus simple.

Sauvegarde MinIO

Synchronisez les données MinIO vers un bucket S3 distant ou un répertoire local :

docker run --rm --network langfuse_default \
  minio/mc alias set local http://minio:9000 minio "$(grep MINIO_ROOT_PASSWORD /opt/langfuse/.env | cut -d= -f2)" && \
  mc mirror local/langfuse /opt/backups/minio-langfuse/

Automatiser avec cron

cat > /etc/cron.d/langfuse-backup << 'EOF'
0 3 * * * root cd /opt/langfuse && docker compose exec -T postgres pg_dump -U langfuse langfuse | gzip > /opt/backups/langfuse-pg-$(date +\%F).sql.gz
0 4 * * * root find /opt/backups -name "langfuse-pg-*.sql.gz" -mtime +14 -delete
EOF
chmod 644 /etc/cron.d/langfuse-backup

Durcissement pour la production

Procédure de mise à jour

Téléchargez les nouvelles images et redémarrez :

cd /opt/langfuse
docker compose pull
docker compose up -d

Langfuse exécute les migrations de base de données automatiquement au démarrage. Consultez les logs du conteneur web après une mise à jour :

docker compose logs langfuse-web --tail 20

Cherchez Ready dans la sortie. Si les migrations échouent, le conteneur ne démarrera pas. Revenez en arrière en fixant le tag de version précédent dans docker-compose.yml (par exemple langfuse/langfuse:3.x.x).

Surveiller Langfuse lui-même

Le point de santé à /api/public/health renvoie 200 OK quand l'API est fonctionnelle. Ajoutez ?failIfDatabaseUnavailable=true pour une vérification plus profonde incluant la connectivité à la base de données.

Surveillez l'utilisation disque du volume ClickHouse. C'est le composant qui grossit le plus vite :

docker system df -v | grep clickhouse

Surveillez la mémoire des conteneurs avec :

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"

Pare-feu

Seuls les ports 22 (SSH), 80 et 443 doivent être ouverts. Tous les ports de bases de données sont liés à 127.0.0.1 dans le fichier compose, mais un pare-feu ajoute une couche de défense Comment configurer un pare-feu Linux avec UFW et nftables sur un VPS :

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable

Langfuse vs LangSmith vs Opik

Fonctionnalité Langfuse LangSmith Opik
Licence MIT (open source) Propriétaire Apache 2.0
Auto-hébergement Docker Compose / K8s Licence Enterprise uniquement Docker Compose / K8s
Stockage des traces Votre infrastructure Cloud LangChain Votre infrastructure
Framework d'évaluation Externe (DeepEval, etc.) Intégré Intégré
OpenTelemetry Support natif Non Partiel

Langfuse et Opik sont les deux options open source viables pour l'auto-hébergement. Langfuse a une communauté plus large et plus d'intégrations. LangSmith nécessite une licence Enterprise pour l'auto-hébergement. Consultez la documentation d'auto-hébergement de Langfuse pour les dernières options de déploiement.

Dépannage

Le conteneur reste unhealthy : Vérifiez docker compose logs <service> --tail 100. Causes fréquentes : mauvais mot de passe dans .env (ClickHouse est sensible à la casse sur les noms d'utilisateur), ou MinIO qui échoue à initialiser les buckets au premier démarrage. Redémarrez la pile avec docker compose down && docker compose up -d.

Le conteneur web plante en boucle avec "invalid port number" : Votre mot de passe PostgreSQL contient des caractères spéciaux (/, +, =) qui cassent l'URL de connexion postgresql://. Regénérez tous les mots de passe avec openssl rand -hex 32 au lieu de -base64, puis lancez docker compose down -v && docker compose up -d pour réinitialiser avec les nouveaux identifiants.

Le conteneur web plante avec "CLICKHOUSE_MIGRATION_URL is not configured" : Ajoutez CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 aux sections environment de langfuse-web et langfuse-worker. Cela utilise le protocole natif ClickHouse sur le port 9000, distinct de l'API HTTP sur le port 8123.

Erreurs 502/504 à travers le reverse proxy : Réglez KEEP_ALIVE_TIMEOUT dans le conteneur web Langfuse à une valeur supérieure au timeout d'inactivité de votre reverse proxy. Caddy utilise un keepalive de 30s par défaut. Nous avons réglé Langfuse à 70s et Caddy à 75s.

L'arrêt du worker prend longtemps : Sous charge, le worker vide sa file d'attente avant de s'arrêter. Cela peut prendre jusqu'à une heure. Pour un arrêt plus rapide, réduisez d'abord à zéro : docker compose stop langfuse-worker, attendez que la file se vide, puis continuez.

Le disque ClickHouse se remplit : Configurez la rétention des données. Langfuse fournit des paramètres de rétention intégrés dans le tableau de bord sous Settings > Data Retention. Configurez le TTL des traces selon votre capacité de stockage.

Logs : Tous les conteneurs Langfuse écrivent sur stdout. Consultez-les avec :

journalctl -u docker -f
docker compose logs -f

AIOps sur un VPS : gestion de serveur par IA avec des outils open source Héberger des agents IA sur un VPS Construire et auto-héberger un serveur MCP sur un VPS [-> docker-compose-multi-service-vps]