Autoalojar Langfuse en un VPS para observabilidad LLM

12 min de lectura·Matthieu·llm-observabilityci-cddeepevaldocker-composelangfuse|

Despliega Langfuse v3 con Docker Compose en tu propio VPS. Traza llamadas LLM, monitoriza costes, ejecuta evaluaciones automatizadas con DeepEval e integra puertas de calidad en tu pipeline CI/CD.

Langfuse es una plataforma open source de observabilidad LLM. Traza cada llamada LLM de tu aplicación: latencia, uso de tokens, coste y pares prompt/completado. Autoalojarlo con Docker Compose mantiene todos los datos de traza en tu infraestructura. Sin facturación por evento. Sin datos que salgan de tu red.

Este tutorial cubre el ciclo completo: desplegar Langfuse v3, añadir TLS, instrumentar aplicaciones Python y TypeScript, y construir pipelines de evaluación automatizados con DeepEval integrados en CI/CD.

¿Qué necesita Langfuse v3 para funcionar en un VPS?

Langfuse v3 ejecuta seis contenedores: la interfaz web, un worker asíncrono, PostgreSQL para metadatos, ClickHouse para analítica de trazas, Redis para colas y MinIO para almacenamiento de objetos. Este es un cambio significativo respecto a v2, que solo necesitaba PostgreSQL.

Componente Función Puerto por defecto RAM base
langfuse-web Interfaz web y API 3000 ~512 MB
langfuse-worker Procesamiento asíncrono de eventos 3030 ~512 MB
PostgreSQL 17 Metadatos transaccionales 5432 ~256 MB
ClickHouse Analítica OLAP de trazas 8123 (HTTP), 9000 (nativo) ~1 GB
Redis 7 Cola y caché 6379 ~128 MB
MinIO Almacenamiento de objetos/medios 9000 (API), 9001 (consola) ~256 MB

Reserva al menos 4 vCPU y 8 GB de RAM. Un Virtua Cloud VCS-8 (4 vCPU, 8 GB RAM, NVMe) lo maneja sin problemas. Empieza con 100 GB de disco. ClickHouse crece aproximadamente 1-2 GB por millón de trazas dependiendo del tamaño de prompts/completados.

Dimensionamiento por escala

Trazas/mes Crecimiento disco/mes VPS recomendado
< 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 Dedicado / Kubernetes

Requisitos previos

¿Cómo despliego Langfuse con Docker Compose?

Clona el repositorio oficial y usa el docker-compose.yml proporcionado como punto de partida. El paso clave es generar secretos reales en lugar de usar los valores de ejemplo.

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

Crea el archivo de entorno con secretos generados:

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

Ahora sustituye los valores temporales por valores aleatorios reales. Usa openssl rand -hex en lugar de -base64 porque la salida base64 contiene caracteres /, + y = que rompen las URLs de conexión 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

Bloquea el archivo. Solo root debería poder leerlo:

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

¿Cómo soluciono el conflicto de puerto 9000 entre MinIO y ClickHouse?

Tanto MinIO como ClickHouse usan el puerto 9000 por defecto. El docker-compose.yml oficial ya mapea el puerto API de MinIO al 9090 en el host (9090:9000), evitando el conflicto. Si escribes un archivo compose personalizado, asegúrate de remapear uno de ellos.

El archivo compose oficial también vincula los puertos de infraestructura solo a 127.0.0.1 (PostgreSQL, ClickHouse nativo, Redis, consola MinIO), impidiendo el acceso externo. El único puerto expuesto en todas las interfaces es el 3000 para la interfaz web, que pondremos detrás de un reverse proxy.

Crea el 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:

Algunos puntos a tener en cuenta en este archivo compose:

  • El contenedor web escucha en 127.0.0.1:3000 en lugar de 0.0.0.0:3000. Todo el tráfico pasa por el reverse proxy.
  • CLICKHOUSE_MIGRATION_URL usa el protocolo nativo (clickhouse://) en el puerto 9000, mientras que CLICKHOUSE_URL usa HTTP en el puerto 8123. Ambos son necesarios. Si falta CLICKHOUSE_MIGRATION_URL, el contenedor web se cae al arrancar.
  • Redis usa variables separadas REDIS_HOST, REDIS_PORT y REDIS_AUTH en lugar de una cadena de conexión. Este es el formato que espera Langfuse v3.
  • El entrypoint de MinIO crea el directorio del bucket langfuse en el primer arranque (mkdir -p /data/langfuse). Sin esto, las subidas S3 fallan hasta que el bucket exista.
  • ClickHouse se ejecuta como user: "101:101" para coincidir con el usuario clickhouse interno del contenedor.
  • Redis se ejecuta con --maxmemory-policy noeviction para prevenir pérdida de datos cuando la memoria es escasa. Langfuse depende de Redis para su cola de trabajos, y la expulsión de claves causaría pérdida silenciosa de datos.

Arranca el stack:

docker compose up -d

Espera unos 2-3 minutos para que todos los contenedores se inicialicen. ClickHouse y PostgreSQL ejecutan migraciones en el primer arranque. Comprueba el estado:

docker compose ps

Los seis contenedores deberían mostrar Up. Los contenedores de infraestructura (postgres, clickhouse, minio, redis) muestran (healthy). Los contenedores langfuse-web y langfuse-worker no definen healthchecks, así que muestran Up sin etiqueta de salud. Si algún contenedor se reinicia en bucle, revisa sus logs:

docker compose logs langfuse-web --tail 50

Consulta el endpoint de salud para confirmar que la API funciona y la conexión a la base de datos está operativa:

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

¿Cómo añado TLS a una instancia Langfuse autoalojada?

Pon Caddy delante de Langfuse. Caddy gestiona automáticamente la obtención de certificados TLS de Let's Encrypt. Sin tareas cron de certbot, sin renovación manual.

Instala 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

Crea el 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

El keepalive 75s está configurado más alto que el KEEP_ALIVE_TIMEOUT=70 de Langfuse para evitar que el reverse proxy mantenga conexiones obsoletas. Esta discrepancia es la causa principal de los errores 502/504 intermitentes que sufren muchos autoalojadores. La directiva header -Server elimina la información de versión de las respuestas.

systemctl enable --now caddy

enable hace que Caddy sobreviva a los reinicios. --now lo arranca inmediatamente.

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

Desde tu máquina local, prueba el endpoint TLS:

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

Configurar autenticación y claves API

Abre https://langfuse.example.com en tu navegador. Crea la primera cuenta de usuario. Esta se convierte en la cuenta de administrador.

Después de iniciar sesión:

  1. Crea un nuevo proyecto (p. ej., "production")
  2. Ve a Settings > API Keys
  3. Haz clic en Create API Key
  4. Guarda la Public Key y la Secret Key. Necesitas ambas para instrumentar tus aplicaciones.

La clave pública identifica tu proyecto. La clave secreta autentica las escrituras. Trata la clave secreta como una contraseña de base de datos.

¿Cómo instrumento una aplicación Python para enviar trazas a Langfuse?

El SDK Python de Langfuse está construido sobre OpenTelemetry. El decorador @observe() crea automáticamente trazas y spans para las funciones decoradas. Las llamadas anidadas producen spans anidados en la interfaz de Langfuse.

Instala el SDK:

pip install langfuse openai

Configura las variables de entorno apuntando a tu instancia autoalojada:

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

Aquí tienes una aplicación instrumentada:

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

El @observe() en run_pipeline crea la traza raíz. answer_question y retrieve_context se convierten en spans anidados. El import parcheado de openai (from langfuse.openai import openai) captura automáticamente el nombre del modelo, el conteo de tokens, la latencia y el coste como un span de generación. El gestor de contexto propagate_attributes adjunta metadatos de usuario y sesión a todas las observaciones anidadas.

Después de ejecutar este script, abre el dashboard de Langfuse. El Trace Explorer muestra el árbol de llamadas completo con el tiempo de cada span.

¿Cómo instrumento una aplicación TypeScript?

Instala el SDK TypeScript de Langfuse y el SDK Node de OpenTelemetry:

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

Configura las mismas variables de entorno:

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

Llama a forceFlush() en el span processor antes de que las funciones serverless o scripts terminen para prevenir pérdida de datos.

Recorrido por el dashboard

El dashboard de Langfuse ofrece cuatro vistas principales:

Trace Explorer muestra trazas de peticiones individuales. Haz clic en cualquier traza para ver el árbol completo de spans: qué funciones se ejecutaron, cuánto tardó cada una y los prompts/completados exactos enviados al LLM. Filtra por ID de usuario, tags o rango temporal.

Seguimiento de costes desglosa el gasto por modelo. Ves qué modelos consumen más tokens y qué llamadas API son caras. Úsalo para identificar prompts que desperdician tokens en completados innecesariamente largos.

Percentiles de latencia (p50, p90, p99) por endpoint o nombre de span. Si tu p99 se dispara, puedes explorar las trazas más lentas para encontrar el cuello de botella.

Tendencias de uso de tokens a lo largo del tiempo. Vigila los saltos inesperados que indican un cambio de prompt o un bug que genera salidas más largas.

¿Cómo configuro evaluación LLM automatizada con DeepEval y Langfuse?

DeepEval es un framework open source de evaluación LLM que puntúa las salidas de modelos en métricas como alucinación, fidelidad y relevancia. Combinado con Langfuse, puedes obtener trazas de producción, ejecutar evaluaciones sobre ellas y enviar las puntuaciones de vuelta al dashboard de Langfuse.

Instala DeepEval:

pip install deepeval

Métricas de evaluación

Métrica Qué mide Rango de puntuación Cuándo usarla
Hallucination Exactitud factual contra el contexto proporcionado 0-1 (1 = sin alucinación) Pipelines RAG
Faithfulness Si la salida se alinea con el contexto de recuperación 0-1 (1 = fiel) Pipelines RAG
Answer Relevancy Si la respuesta contesta la pregunta 0-1 (1 = relevante) Cualquier app LLM
G-Eval Criterios personalizados via LLM-as-judge 0-1 Controles de calidad personalizados

Script de evaluación

Este script obtiene trazas recientes de Langfuse, ejecuta métricas DeepEval y envía las puntuaciones de vuelta:

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

Después de ejecutarlo, las puntuaciones de evaluación aparecen en el dashboard de Langfuse junto a las trazas originales. Puedes filtrar trazas por puntuación para encontrar salidas de baja calidad.

¿Cómo ejecuto evaluaciones LLM en un pipeline CI/CD?

Integra DeepEval en GitHub Actions para detectar regresiones de calidad antes de que lleguen a producción. El workflow ejecuta tu suite de evaluación contra un dataset de prueba en cada pull request.

Crea .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/

Crea el archivo 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])

El workflow solo se activa cuando cambian las plantillas de prompts o el código LLM. Si alguna métrica cae por debajo del umbral, el build de la PR falla. Los desarrolladores ven en los logs de GitHub Actions qué caso de test falló y por cuánto.

¿Cómo hago backup de PostgreSQL y ClickHouse para Langfuse?

Autoalojar sin backups es una irresponsabilidad. PostgreSQL contiene cuentas de usuario, configuración de proyectos y claves API. ClickHouse contiene todos los datos de traza. MinIO contiene las subidas de medios y los exports por lotes.

Backup de PostgreSQL

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

Backup de ClickHouse

Exporta tablas individualmente usando el cliente 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 también soporta el comando BACKUP DATABASE para backups completos, pero requiere configurar un allowed_disk en la configuración del servidor ClickHouse. Para despliegues Docker, la exportación por tabla anterior es más sencilla.

Backup de MinIO

Sincroniza los datos de MinIO a un bucket remoto compatible con S3 o a un directorio 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/

Automatizar con 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

Endurecimiento para producción

Procedimiento de actualización

Descarga las nuevas imágenes y reinicia:

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

Langfuse ejecuta las migraciones de base de datos automáticamente al arrancar. Revisa los logs del contenedor web después de una actualización:

docker compose logs langfuse-web --tail 20

Busca Ready en la salida. Si las migraciones fallan, el contenedor no arrancará. Revierte fijando la etiqueta de versión anterior en docker-compose.yml (p. ej., langfuse/langfuse:3.x.x).

Monitorizar Langfuse

El endpoint de salud en /api/public/health devuelve 200 OK cuando la API está funcional. Añade ?failIfDatabaseUnavailable=true para una comprobación más profunda que incluye la conectividad de base de datos.

Monitoriza el uso de disco del volumen ClickHouse. Es el componente que crece más rápido:

docker system df -v | grep clickhouse

Monitoriza la memoria de los contenedores con:

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

Firewall

Solo los puertos 22 (SSH), 80 y 443 deben estar abiertos. Todos los puertos de bases de datos están vinculados a 127.0.0.1 en el archivo compose, pero un firewall añade defensa en profundidad Cómo configurar un firewall en un VPS Linux con UFW y nftables:

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

Característica Langfuse LangSmith Opik
Licencia MIT (open source) Propietaria Apache 2.0
Autoalojamiento Docker Compose / K8s Solo licencia Enterprise Docker Compose / K8s
Almacenamiento de trazas Tu infraestructura Cloud de LangChain Tu infraestructura
Framework de evaluación Externo (DeepEval, etc.) Integrado Integrado
OpenTelemetry Soporte nativo No Parcial

Langfuse y Opik son las dos opciones open source viables para autoalojamiento. Langfuse tiene una comunidad más grande y más integraciones. LangSmith requiere una licencia Enterprise para autoalojamiento. Consulta la documentación de autoalojamiento de Langfuse para las últimas opciones de despliegue.

Solución de problemas

El contenedor se queda unhealthy: Revisa docker compose logs <service> --tail 100. Causas comunes: contraseña incorrecta en .env (ClickHouse distingue mayúsculas y minúsculas en nombres de usuario), o MinIO falla al inicializar buckets en el primer arranque. Reinicia el stack con docker compose down && docker compose up -d.

El contenedor web se cae en bucle con "invalid port number": Tu contraseña de PostgreSQL contiene caracteres especiales (/, +, =) que rompen la URL de conexión postgresql://. Regenera todas las contraseñas usando openssl rand -hex 32 en lugar de -base64, luego ejecuta docker compose down -v && docker compose up -d para reiniciar con las nuevas credenciales.

El contenedor web se cae con "CLICKHOUSE_MIGRATION_URL is not configured": Añade CLICKHOUSE_MIGRATION_URL=clickhouse://clickhouse:9000 a las secciones de environment de langfuse-web y langfuse-worker. Esto usa el protocolo nativo de ClickHouse en el puerto 9000, separado de la API HTTP en el puerto 8123.

Errores 502/504 a través del reverse proxy: Configura KEEP_ALIVE_TIMEOUT en el contenedor web de Langfuse a un valor mayor que el timeout de inactividad de tu reverse proxy. Caddy usa 30s de keepalive por defecto. Nosotros configuramos Langfuse a 70s y Caddy a 75s.

El apagado del worker tarda mucho: Bajo carga, el worker vacía su cola antes de detenerse. Esto puede tardar hasta una hora. Para un apagado más rápido, reduce primero a cero: docker compose stop langfuse-worker, espera a que la cola se vacíe, luego continúa.

El disco de ClickHouse se llena: Configura la retención de datos. Langfuse proporciona ajustes de retención integrados en el dashboard bajo Settings > Data Retention. Configura el TTL de trazas según tu capacidad de almacenamiento.

Logs: Todos los contenedores Langfuse escriben a stdout. Consúltalos con:

journalctl -u docker -f
docker compose logs -f

AIOps en un VPS: gestión de servidores con IA y herramientas open source Alojar agentes de IA en un VPS Construir y alojar un servidor MCP personalizado en un VPS [-> docker-compose-multi-service-vps]