Langfuse zelf hosten op een VPS voor LLM-observability
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
- Een VPS met Debian 12 of Ubuntu 24.04 met minimaal 4 vCPU en 8 GB RAM
- Docker en Docker Compose geïnstalleerd Docker in productie op een VPS: wat er misgaat en hoe je het oplost
- Een domeinnaam met een A-record dat naar het IP van je VPS wijst (voor TLS)
- SSH-toegang met sleutelgebaseerde authenticatie SSH beveiligen op een Linux VPS: complete sshd_config gids
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:3000in plaats van0.0.0.0:3000. Al het verkeer gaat via de reverse proxy. CLICKHOUSE_MIGRATION_URLgebruikt het native protocol (clickhouse://) op poort 9000, terwijlCLICKHOUSE_URLHTTP gebruikt op poort 8123. Beide zijn vereist. AlsCLICKHOUSE_MIGRATION_URLontbreekt, crasht de webcontainer bij het opstarten.- Redis gebruikt afzonderlijke
REDIS_HOST-,REDIS_PORT- enREDIS_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 noevictionom 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:
- Maak een nieuw project aan (bijv. "production")
- Ga naar Settings > API Keys
- Klik op Create API Key
- 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]
Klaar om het zelf te proberen?
Deploy uw eigen server in seconden. Linux, Windows of FreeBSD. →