Skip to content

English · Español

Lab 00 — Arranque en frío

El primer arranque en frío de toda la pila. La regla de oro: lo que descubras roto aquí, lo arreglas en su fase de origen, no aquí. La Fase 39 compone — no parchea. Documenta cada error, su fase responsable y el commit del fix. Si al final del lab docker compose up arranca verde en menos de 30 segundos, has terminado.

Objetivo

Desde un checkout fresco de lynx-cortex en el i5-8250U de Borja, levantar el stack completo de la demo en frío y alcanzar salud verde en cada contenedor en 30 segundos. Resolver cada error de configuración faltante en el camino. Comitear el log experimental bajo experiments/39-capstone-bringup/.

Por qué existe este lab

Cada fase previa escribió un contenedor, un archivo de configuración, un servicio. El capstone los compone. Tres cosas se rompen, predeciblemente:

  1. Configuración faltante — la env var MINISERVE_HOST de la Fase 33 no se propaga a docker-compose.
  2. Colisión de puertos — el :9090 por defecto de Prometheus de la Fase 34 choca con un servicio de la Fase 38.
  3. Problemas de volume mount — un path relativo que funcionaba desde cd src/miniserve/ no funciona desde cd infra/compose/.

Cada fix aterriza en el directorio de la fase originaria, con una nota de una línea en el log de experimento de este lab apuntando al commit del fix. No arreglar en la Fase 39. La Fase 39 solo compone.

Entregables

  • infra/compose/full-stack.yml — el docker-compose compuesto para just demo-cold. Borja escribe; este lab provee la plantilla de partida (§3 abajo).
  • infra/grafana/datasources/prometheus.yaml — el archivo de provisioning de datasource para que los paneles del dashboard resuelvan contra el Prometheus correcto.
  • infra/grafana/dashboards/capstone.json — un dashboard esqueleto con los 10 paneles de Teoría 03 §dashboard, incluso si algunos muestran "No Data" en este momento.
  • Recetas del Justfile demo-cold y demo, cableadas al archivo de compose y a scripts/demo/run.py.
  • experiments/39-capstone-bringup/log.md — log cronológico de cada error, cada fix, cada hash de commit.
  • docs/DONE_ENOUGH.md — los ≤ 20 checks binarios (borrador).
  • tests/integration/test_stack_healthy.py — un pytest que ejecuta just demo-cold, hace polling a todos los endpoints /healthz y afirma verde dentro de 30 s.

Paso 1 — Auditar el estado inicial

$ cd lynx-cortex
$ git log --oneline -1
$ uv sync --frozen
$ rg -l 'docker' infra/      # listar archivos de compose existentes

Esperado: cada fase previa contribuyó un snippet de compose de un solo servicio bajo infra/compose/. El trabajo de la Fase 39 es mezclarlos en full-stack.yml.

Los snippets de compose a mezclar:

Origen Servicio Fase
infra/compose/miniserve.yml miniserve 33
infra/compose/prometheus.yml prometheus 34
infra/compose/grafana.yml grafana 34
infra/compose/tempo.yml tempo 34
infra/compose/mlflow.yml mlflow-tracking 38
infra/compose/langfuse.yml (opcional) langfuse 38

Si falta alguno de estos, stop: deberían haberse escrito en su fase originaria. Abre el PHASE_NN_REPORT.md de la fase originaria y anota el hueco como carry-over a arreglar fuera de la Fase 39.

Paso 2 — Mezclar en full-stack.yml

Plantilla de partida:

# infra/compose/full-stack.yml
name: lynx-cortex-demo
services:
  miniserve:
    extends:
      file: ./miniserve.yml
      service: miniserve
    depends_on:
      prometheus:
        condition: service_healthy
      tempo:
        condition: service_healthy

  prometheus:
    extends:
      file: ./prometheus.yml
      service: prometheus

  grafana:
    extends:
      file: ./grafana.yml
      service: grafana
    depends_on:
      prometheus:
        condition: service_healthy
    volumes:
      - ./grafana/datasources:/etc/grafana/provisioning/datasources:ro
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards:ro

  tempo:
    extends:
      file: ./tempo.yml
      service: tempo

  mlflow-tracking:
    extends:
      file: ./mlflow.yml
      service: mlflow-tracking

El patrón extends preserva los archivos de compose por fase (cada fase sigue siendo dueña de la definición de su servicio) mientras los compone en uno. No duplicar definiciones de servicio — la duplicación invita a la deriva.

Paso 3 — Primer arranque en frío

$ just demo-cold-up      # docker compose -f infra/compose/full-stack.yml up -d
$ docker compose -f infra/compose/full-stack.yml ps

Resultados esperados (ordenados por probabilidad):

  1. Colisión de puertosprometheus y otro servicio de Borja ambos quieren :9090. Arreglar editando el compose originario de la Fase 34 para permitir env var PROMETHEUS_PORT; default :9090 pero anulable. El fix aterriza en la Fase 34, no en la 39. Log: experiments/39-capstone-bringup/log.md2026-06-XX 10:32 — prometheus :9090 collision with system grafana — fix: Phase 34 compose adds PROMETHEUS_PORT=9091 — commit abc1234.
  2. Env var faltanteminiserve no ve OTEL_EXPORTER_OTLP_ENDPOINT. Arreglar en el snippet de compose de la fase originaria.
  3. Path de volume mount — el path de provisioning del datasource de Grafana es ./grafana/datasources/ relativo; desde infra/compose/full-stack.yml resuelve correctamente solo si el directorio de trabajo del archivo compose es infra/compose/. Verificar con docker compose config.

Cada error se loguea con: timestamp, síntoma, causa raíz, fase que lo arregla, hash de commit.

Paso 4 — Health checks

Añadir healthcheck: a cada servicio en su archivo compose originario (si no está ya presente):

# miniserve
healthcheck:
  test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
  interval: 5s
  timeout: 3s
  retries: 6
  start_period: 5s

Cada servicio debe alcanzar healthy dentro de 30 s desde docker compose up. El test de integración test_stack_healthy.py hace polling de docker compose ps --format json y afirma que todos los servicios muestran health: healthy dentro del límite.

# tests/integration/test_stack_healthy.py
import json, subprocess, time

def test_full_stack_reaches_healthy_within_30s():
    subprocess.run(["just", "demo-cold-up"], check=True)
    deadline = time.time() + 30
    while time.time() < deadline:
        ps = subprocess.run(
            ["docker", "compose", "-f", "infra/compose/full-stack.yml", "ps", "--format", "json"],
            check=True, capture_output=True, text=True,
        )
        statuses = [json.loads(line) for line in ps.stdout.splitlines() if line.strip()]
        if statuses and all(s.get("Health") == "healthy" for s in statuses):
            return
        time.sleep(2)
    subprocess.run(["just", "demo-cold-down"])
    raise AssertionError("stack did not reach healthy within 30s")

Paso 5 — Provisioning de Grafana

infra/grafana/datasources/prometheus.yaml:

apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    editable: false
  - name: Tempo
    type: tempo
    access: proxy
    url: http://tempo:3200
    editable: false

infra/grafana/dashboards/capstone.json se construye:

  1. Levantando el stack.
  2. Abriendo Grafana en :3000, credenciales por defecto.
  3. Construyendo manualmente los 10 paneles de Teoría 03 §dashboard (algunos quedarán vacíos; eso está bien para el esqueleto).
  4. Exportando el JSON del dashboard vía la UI de Grafana (Share → Export → Save to file).
  5. Comiteando el archivo bajo infra/grafana/dashboards/.

El provisioning de Grafana luego lo auto-importa en el siguiente arranque del stack.

Principio esqueleto-primero: un dashboard con 10 paneles diciendo "No Data" es informativo (muestra el contrato). Un dashboard con 5 paneles que se pueblan pero le faltan los otros 5 oculta el contrato. Comitear el esqueleto completo aunque la mitad esté vacía en esta etapa.

Paso 6 — Primer borrador de DONE_ENOUGH.md

Escribe los ≤ 20 checks binarios de la Teoría 05. Usa esta plantilla:

# Phase 39 — Capstone DoD checklist

Every check is binary, automated, and runs as part of `just demo`. If any
check fails, the demo exits non-zero and the phase report is blocked.

| ID | Statement | How to verify | Owner phase |
|---|---|---|---|
| DE-001 | Stack starts within 30 s | `tests/integration/test_stack_healthy.py` | 39 |
| DE-002 | `miniserve` responds on :8080 within 5 s | `curl :8080/healthz` in demo script | 33 |
| ... | ... | ... | ... |

15 filas de la Teoría 05; añadir 5 más cubriendo: (a) RAG retrieval devuelve ≥ 1 chunk; (b) el contexto de trace propaga a través de la frontera MCP; © la identidad de descomposición de coste se mantiene; (d) el provisioning del dashboard de Grafana tiene cero errores al arrancar; (e) el transcript.jsonl de la demo está bien formado como JSON-lines.

Paso 7 — Cablear las recetas del Justfile

# Extracto del Justfile
demo-cold-up:
    docker compose -f infra/compose/full-stack.yml up -d

demo-cold-down:
    docker compose -f infra/compose/full-stack.yml down -v --remove-orphans

demo-cold: demo-cold-up
    uv run python scripts/demo/run.py
    just demo-cold-down

demo: demo-cold-up
    uv run python scripts/demo/run.py

just demo deja el stack arriba (uso interactivo); just demo-cold lo derriba (CI).

Paso 8 — Cinco ejecuciones consecutivas

El DoD requiere éxito 5-de-5. Ejecuta:

$ for i in $(seq 1 5); do
    echo "=== run $i ==="
    just demo-cold || break
done

Si alguna ejecución falla, captura el fallo en experiments/39-capstone-bringup/log.md y arregla en la fase originaria antes de continuar. No arreglar en la Fase 39.

Qué pinta tiene "hecho"

  • full-stack.yml existe y mezcla todos los snippets de compose por fase vía extends.
  • just demo-cold-up lleva cada servicio a health: healthy en 30 s.
  • tests/integration/test_stack_healthy.py pasa.
  • infra/grafana/datasources/prometheus.yaml y el datasource tempo existen.
  • infra/grafana/dashboards/capstone.json esqueleto comiteado (10 paneles, aunque la mitad muestre "No Data").
  • docs/DONE_ENOUGH.md redactado con ≤ 20 filas.
  • just demo-cold-down elimina limpiamente todos los contenedores y volúmenes.
  • Cinco ejecuciones consecutivas de just demo-cold tienen éxito.
  • experiments/39-capstone-bringup/log.md lista cada error encontrado, con el commit del fix y la fase originaria.

Trampas comunes

  1. Arreglar cosas en la Fase 39. Tentador; mal. El fix pertenece a la fase originaria. La Fase 39 solo compone.
  2. Hardcodear puertos. Usa env vars con defaults; la demo corre en la máquina de Borja y en hardware de CI que puede tener conflictos de puerto.
  3. Comitear datos personales de mlruns/. Esa es la trampa de supply-chain del §5 #1 del Plan. Añadir a .gitignore antes del primer commit si no está ya.
  4. Olvidar --remove-orphans en down. Un contenedor sobrante de una ejecución previa bloquea el siguiente arranque; la idempotencia se rompe.
  5. Confiar en "arrancó" sin health checks. Un contenedor que arranca no es un contenedor que está listo. El healthcheck es el contrato.

Siguiente: lab/01-end-to-end-grammar-tutor-request.md — petición única a través de cada capa; árbol de trace capturado; identidad de coste verificada.