Langfuse zelf hosten op een VPS voor LLM-observability

11 min leestijd·Matthieu·llm-observabilityci-cddeepevaldocker-composelangfuse|

Implementeer Langfuse v3 met Docker Compose op je eigen VPS. Traceer LLM-aanroepen, volg kosten, voer geautomatiseerde evaluaties uit met DeepEval en integreer quality gates in je CI/CD-pipeline.

Langfuse is een open-source platform voor LLM-observability. Het traceert elke LLM-aanroep in je applicatie: latentie, tokengebruik, kosten en prompt/completion-paren. Zelf hosten met Docker Compose houdt alle tracedata op je eigen infrastructuur. Geen afrekening per event. Geen data die je netwerk verlaat.

Deze tutorial behandelt de volledige levenscyclus: Langfuse v3 implementeren, TLS toevoegen, Python- en TypeScript-applicaties instrumenteren, en geautomatiseerde evaluatiepipelines bouwen met DeepEval geïntegreerd in CI/CD.

Wat heeft Langfuse v3 nodig om op een VPS te draaien?

Langfuse v3 draait zes containers: de webinterface, een asynchrone worker, PostgreSQL voor metadata, ClickHouse voor trace-analytics, Redis voor queuing en MinIO voor blob-opslag. Dit is een belangrijke verandering ten opzichte van v2, dat alleen PostgreSQL nodig had.

Component Doel Standaard poort RAM-basislijn
langfuse-web Webinterface en API 3000 ~512 MB
langfuse-worker Asynchrone eventverwerking 3030 ~512 MB
PostgreSQL 17 Transactionele metadata 5432 ~256 MB
ClickHouse OLAP trace-analytics 8123 (HTTP), 9000 (natief) ~1 GB
Redis 7 Queue en cache 6379 ~128 MB
MinIO Blob-/mediaopslag 9000 (API), 9001 (console) ~256 MB

Reserveer minimaal 4 vCPU en 8 GB RAM. Een Virtua Cloud VCS-8 (4 vCPU, 8 GB RAM, NVMe) handelt dit comfortabel af. Begin met 100 GB schijfruimte. ClickHouse groeit ongeveer 1-2 GB per miljoen traces, afhankelijk van prompt/completion-groottes.

Capaciteitsplanning per schaal

Traces/maand Schijfgroei/maand Aanbevolen VPS
< 100K ~500 MB 4 vCPU / 8 GB
100K - 1M 1-2 GB 4 vCPU / 8 GB
1M - 10M 10-20 GB 8 vCPU / 16 GB
> 10M 50+ GB Dedicated / Kubernetes

Vereisten

Hoe implementeer ik Langfuse met Docker Compose?

Kloon de officiële repository en gebruik de meegeleverde docker-compose.yml als uitgangspunt. De cruciale stap is het genereren van echte secrets in plaats van de standaardwaarden.

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

Maak het omgevingsbestand aan met gegenereerde secrets:

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

Vervang nu de tijdelijke waarden door echte willekeurige waarden. Gebruik openssl rand -hex in plaats van -base64 omdat base64-uitvoer de tekens /, + en = bevat die PostgreSQL-verbindings-URL's breken:

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

Vergrendel het bestand. Alleen root mag het lezen:

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

Hoe los ik het poort-9000-conflict tussen MinIO en ClickHouse op?

Zowel MinIO als ClickHouse gebruiken standaard poort 9000. De officiële docker-compose.yml mapt de API-poort van MinIO al naar 9090 op de host (9090:9000), waardoor het conflict wordt vermeden. Als je een aangepast compose-bestand schrijft, zorg er dan voor dat je een van beide hermapt.

Het officiële compose-bestand bindt infrastructuurpoorten ook alleen aan 127.0.0.1 (PostgreSQL, ClickHouse natief, Redis, MinIO-console), waardoor externe toegang wordt voorkomen. De enige poort die op alle interfaces wordt blootgesteld is 3000 voor de webinterface, die we achter een reverse proxy plaatsen.

Maak de docker-compose.yml aan:

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:

Een paar opmerkingen bij dit compose-bestand:

  • De webcontainer luistert op 127.0.0.1:3000 in plaats van 0.0.0.0:3000. Al het verkeer gaat via de reverse proxy.
  • CLICKHOUSE_MIGRATION_URL gebruikt het native protocol (clickhouse://) op poort 9000, terwijl CLICKHOUSE_URL HTTP gebruikt op poort 8123. Beide zijn vereist. Als CLICKHOUSE_MIGRATION_URL ontbreekt, crasht de webcontainer bij het opstarten.
  • Redis gebruikt afzonderlijke REDIS_HOST-, REDIS_PORT- en REDIS_AUTH-variabelen in plaats van een verbindingsstring. Dit is het formaat dat Langfuse v3 verwacht.
  • Het MinIO-entrypoint maakt de langfuse-bucketdirectory aan bij de eerste start (mkdir -p /data/langfuse). Zonder dit mislukken S3-uploads totdat de bucket bestaat.
  • ClickHouse draait als user: "101:101" om overeen te komen met de interne clickhouse-gebruiker van de container.
  • Redis draait met --maxmemory-policy noeviction om gegevensverlies te voorkomen wanneer het geheugen krap is. Langfuse vertrouwt op Redis voor zijn taakwachtrij, en het verwijderen van sleutels zou stil gegevensverlies veroorzaken.

Start de stack:

docker compose up -d

Wacht ongeveer 2-3 minuten totdat alle containers zijn geïnitialiseerd. ClickHouse en PostgreSQL voeren migraties uit bij de eerste start. Controleer de status:

docker compose ps

Alle zes containers moeten Up tonen. De infrastructuurcontainers (postgres, clickhouse, minio, redis) tonen (healthy). De langfuse-web- en langfuse-worker-containers definiëren geen healthchecks en tonen Up zonder gezondheidslabel. Als een container herstart in een lus, bekijk dan de logs:

docker compose logs langfuse-web --tail 50

Raadpleeg het health-endpoint om te bevestigen dat de API draait en de databaseverbinding werkt:

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

Hoe voeg ik TLS toe aan een zelf gehoste Langfuse-instantie?

Zet Caddy voor Langfuse. Caddy regelt automatisch de TLS-certificaatvoorziening van Let's Encrypt. Geen certbot-cronjobs, geen handmatige vernieuwing.

Installeer 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

Maak het Caddyfile aan:

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

De keepalive 75s is hoger ingesteld dan Langfuses KEEP_ALIVE_TIMEOUT=70 om te voorkomen dat de reverse proxy verouderde verbindingen aanhoudt. Dit verschil is de hoofdoorzaak van de intermitterende 502/504-fouten die veel zelfhosters tegenkomen. De header -Server-richtlijn verwijdert versie-informatie uit de responses.

systemctl enable --now caddy

enable zorgt ervoor dat Caddy herstart overleeft. --now start het onmiddellijk.

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

Test vanaf je lokale machine het TLS-endpoint:

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

Authenticatie en API-sleutels configureren

Open https://langfuse.example.com in je browser. Maak het eerste gebruikersaccount aan. Dit wordt het beheerdersaccount.

Na het inloggen:

  1. Maak een nieuw project aan (bijv. "production")
  2. Ga naar Settings > API Keys
  3. Klik op Create API Key
  4. Sla de Public Key en Secret Key op. Je hebt beide nodig om je applicaties te instrumenteren.

De publieke sleutel identificeert je project. De geheime sleutel authenticeert schrijfacties. Behandel de geheime sleutel als een databasewachtwoord.

Hoe instrumenteer ik een Python-applicatie om traces naar Langfuse te sturen?

De Langfuse Python SDK is gebouwd op OpenTelemetry. De @observe()-decorator maakt automatisch traces en spans aan voor gedecoreerde functies. Geneste aanroepen produceren geneste spans in de Langfuse-interface.

Installeer de SDK:

pip install langfuse openai

Stel de omgevingsvariabelen in die naar je zelf gehoste instantie wijzen:

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

Hier is een geïnstrumenteerde applicatie:

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

De @observe() op run_pipeline maakt de roottrace aan. answer_question en retrieve_context worden geneste spans. De gepatchte openai-import (from langfuse.openai import openai) vangt automatisch modelnaam, tokenaantallen, latentie en kosten op als een generatiespan. De propagate_attributes-contextmanager koppelt gebruikers- en sessiemetadata aan alle geneste observaties.

Na het uitvoeren van dit script, open je het Langfuse-dashboard. De Trace Explorer toont de volledige aanroepboom met timing voor elke span.

Hoe instrumenteer ik een TypeScript-applicatie?

Installeer de Langfuse TypeScript SDK en de OpenTelemetry Node SDK:

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

Stel dezelfde omgevingsvariabelen in:

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 } });
    }
  );
});

Roep forceFlush() aan op de span processor voordat serverless functies of scripts afsluiten om gegevensverlies te voorkomen.

Dashboard-overzicht

Het Langfuse-dashboard biedt vier belangrijke weergaven:

Trace Explorer toont individuele request-traces. Klik op een trace om de volledige spanboom te zien: welke functies draaiden, hoe lang elk duurde en de exacte prompts/completions die naar het LLM werden gestuurd. Filter op gebruikers-ID, tags of tijdsbereik.

Kostentracking splitst de uitgaven per model uit. Je ziet welke modellen de meeste tokens verbruiken en welke API-aanroepen duur zijn. Gebruik dit om prompts te identificeren die tokens verspillen aan onnodig lange completions.

Latentiepercentilen (p50, p90, p99) per endpoint of spannaam. Als je p99 piekt, kun je de langzaamste traces analyseren om het knelpunt te vinden.

Tokengebruikstrends over tijd. Let op onverwachte sprongen die wijzen op een promptwijziging of een bug die langere uitvoer genereert.

Hoe stel ik geautomatiseerde LLM-evaluatie in met DeepEval en Langfuse?

DeepEval is een open-source LLM-evaluatieframework dat modeluitvoer scoort op metrieken zoals hallucinatie, getrouwheid en relevantie. Gecombineerd met Langfuse kun je productietraces ophalen, evaluaties erop uitvoeren en scores terugsturen naar het Langfuse-dashboard.

Installeer DeepEval:

pip install deepeval

Evaluatiemetrieken

Metriek Wat het meet Scorebereik Wanneer gebruiken
Hallucination Feitelijke nauwkeurigheid tegen verstrekte context 0-1 (1 = geen hallucinatie) RAG-pipelines
Faithfulness Of de uitvoer overeenkomt met de ophaalcontext 0-1 (1 = getrouw) RAG-pipelines
Answer Relevancy Of het antwoord de vraag beantwoordt 0-1 (1 = relevant) Elke LLM-app
G-Eval Aangepaste criteria via LLM-as-judge 0-1 Aangepaste kwaliteitscontroles

Evaluatiescript

Dit script haalt recente traces op uit Langfuse, voert DeepEval-metrieken uit en stuurt scores terug:

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

Na uitvoering verschijnen de evaluatiescores in het Langfuse-dashboard naast de oorspronkelijke traces. Je kunt traces filteren op score om uitvoer van lage kwaliteit te vinden.

Hoe voer ik LLM-evaluaties uit in een CI/CD-pipeline?

Integreer DeepEval in GitHub Actions om kwaliteitsregressies op te vangen voordat ze productie bereiken. De workflow voert je evaluatiesuite uit tegen een testdataset bij elk pull request.

Maak .github/workflows/llm-eval.yml aan:

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/

Maak het testbestand tests/test_llm_quality.py aan:

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

De workflow wordt alleen geactiveerd wanneer promptsjablonen of LLM-code wijzigen. Als een metriek onder de drempel zakt, faalt de PR-build. Ontwikkelaars zien in de GitHub Actions-logs welke testcase is gefaald en met hoeveel.

Hoe maak ik een backup van PostgreSQL en ClickHouse voor Langfuse?

Zelf hosten zonder backups is onverantwoord. PostgreSQL bevat gebruikersaccounts, projectinstellingen en API-sleutels. ClickHouse bevat alle tracedata. MinIO bevat media-uploads en batch-exports.

PostgreSQL-backup

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

ClickHouse-backup

Exporteer tabellen individueel met de ClickHouse-client:

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 ondersteunt ook het BACKUP DATABASE-commando voor volledige backups, maar dit vereist het configureren van een allowed_disk in de ClickHouse-serverconfiguratie. Voor Docker-implementaties is de bovenstaande tabelexport eenvoudiger.

MinIO-backup

Synchroniseer MinIO-data naar een remote S3-compatibele bucket of een lokale directory:

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/

Automatiseren met 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

Productiehardening

Updateprocedure

Download nieuwe images en herstart:

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

Langfuse voert databasemigraties automatisch uit bij het opstarten. Controleer de logs van de webcontainer na een update:

docker compose logs langfuse-web --tail 20

Zoek naar Ready in de uitvoer. Als migraties mislukken, start de container niet. Rol terug door de vorige versietag vast te zetten in docker-compose.yml (bijv. langfuse/langfuse:3.x.x).

Langfuse zelf monitoren

Het health-endpoint op /api/public/health retourneert 200 OK wanneer de API functioneel is. Voeg ?failIfDatabaseUnavailable=true toe voor een diepere controle inclusief databaseconnectiviteit.

Monitor het schijfgebruik van het ClickHouse-volume. Het is het snelst groeiende component:

docker system df -v | grep clickhouse

Monitor containergeheugen met:

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

Firewall

Alleen poorten 22 (SSH), 80 en 443 mogen open zijn. Alle databasepoorten zijn gebonden aan 127.0.0.1 in het compose-bestand, maar een firewall voegt verdediging in de diepte toe [-> linux-firewall-ufw-nftables-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

Functie Langfuse LangSmith Opik
Licentie MIT (open source) Propriëtair Apache 2.0
Zelf hosten Docker Compose / K8s Alleen Enterprise-licentie Docker Compose / K8s
Traceopslag Je eigen infrastructuur LangChains cloud Je eigen infrastructuur
Evaluatieframework Extern (DeepEval, etc.) Ingebouwd Ingebouwd
OpenTelemetry Native ondersteuning Nee Gedeeltelijk

Langfuse en Opik zijn de twee levensvatbare open-source opties voor zelf hosten. Langfuse heeft een grotere community en meer integraties. LangSmith vereist een Enterprise-licentie voor zelf hosten. Bekijk de Langfuse self-hosting documentatie voor de laatste implementatieopties.

Probleemoplossing

Container blijft unhealthy: Controleer docker compose logs <service> --tail 100. Veelvoorkomende oorzaken: verkeerd wachtwoord in .env (ClickHouse is hoofdlettergevoelig bij gebruikersnamen), of MinIO dat bij de eerste start geen buckets kan initialiseren. Herstart de stack met docker compose down && docker compose up -d.

Webcontainer crasht in een lus met "invalid port number": Je PostgreSQL-wachtwoord bevat speciale tekens (/, +, =) die de postgresql://-verbindings-URL breken. Genereer alle wachtwoorden opnieuw met openssl rand -hex 32 in plaats van -base64, voer dan docker compose down -v && docker compose up -d uit om te resetten met de nieuwe inloggegevens.

Webcontainer crasht met "CLICKHOUSE_MIGRATION_URL is not configured": Voeg CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 toe aan de environment-secties van zowel langfuse-web als langfuse-worker. Dit gebruikt het native ClickHouse-protocol op poort 9000, gescheiden van de HTTP-API op poort 8123.

502/504-fouten via de reverse proxy: Stel KEEP_ALIVE_TIMEOUT in de Langfuse-webcontainer in op een waarde hoger dan de idle timeout van je reverse proxy. Caddy gebruikt standaard 30s keepalive. Wij stellen Langfuse in op 70s en Caddy op 75s.

Worker-shutdown duurt lang: Onder belasting leegt de worker zijn wachtrij voor het stoppen. Dit kan tot een uur duren. Voor een snellere shutdown, schaal eerst naar nul: docker compose stop langfuse-worker, wacht tot de wachtrij leeg is, ga dan verder.

ClickHouse-schijf loopt vol: Stel dataretentie in. Langfuse biedt ingebouwde retentie-instellingen in het dashboard onder Settings > Data Retention. Configureer de trace-TTL op basis van je opslagcapaciteit.

Logs: Alle Langfuse-containers schrijven naar stdout. Bekijk ze met:

journalctl -u docker -f
docker compose logs -f

AIOps op een VPS: AI-gestuurde serverbeheer met open-source tools AI-agenten zelf hosten op een VPS Een eigen MCP-server bouwen en hosten op een VPS [-> docker-compose-multi-service-vps]