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 uparranca 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:
- Configuración faltante — la env var
MINISERVE_HOSTde la Fase 33 no se propaga a docker-compose. - Colisión de puertos — el
:9090por defecto de Prometheus de la Fase 34 choca con un servicio de la Fase 38. - Problemas de volume mount — un path relativo que funcionaba desde
cd src/miniserve/no funciona desdecd 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 parajust 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
Justfiledemo-coldydemo, cableadas al archivo de compose y ascripts/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 ejecutajust demo-cold, hace polling a todos los endpoints/healthzy 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):
- Colisión de puertos —
prometheusy otro servicio de Borja ambos quieren:9090. Arreglar editando el compose originario de la Fase 34 para permitir env varPROMETHEUS_PORT; default :9090 pero anulable. El fix aterriza en la Fase 34, no en la 39. Log:experiments/39-capstone-bringup/log.md—2026-06-XX 10:32 — prometheus :9090 collision with system grafana — fix: Phase 34 compose adds PROMETHEUS_PORT=9091 — commit abc1234. - Env var faltante —
miniserveno veOTEL_EXPORTER_OTLP_ENDPOINT. Arreglar en el snippet de compose de la fase originaria. - Path de volume mount — el path de provisioning del datasource de Grafana es
./grafana/datasources/relativo; desdeinfra/compose/full-stack.ymlresuelve correctamente solo si el directorio de trabajo del archivo compose esinfra/compose/. Verificar condocker 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:
- Levantando el stack.
- Abriendo Grafana en :3000, credenciales por defecto.
- Construyendo manualmente los 10 paneles de Teoría 03 §dashboard (algunos quedarán vacíos; eso está bien para el esqueleto).
- Exportando el JSON del dashboard vía la UI de Grafana (
Share → Export → Save to file). - 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:
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.ymlexiste y mezcla todos los snippets de compose por fase víaextends. -
just demo-cold-uplleva cada servicio ahealth: healthyen 30 s. -
tests/integration/test_stack_healthy.pypasa. -
infra/grafana/datasources/prometheus.yamly el datasourcetempoexisten. -
infra/grafana/dashboards/capstone.jsonesqueleto comiteado (10 paneles, aunque la mitad muestre "No Data"). -
docs/DONE_ENOUGH.mdredactado con ≤ 20 filas. -
just demo-cold-downelimina limpiamente todos los contenedores y volúmenes. - Cinco ejecuciones consecutivas de
just demo-coldtienen éxito. -
experiments/39-capstone-bringup/log.mdlista cada error encontrado, con el commit del fix y la fase originaria.
Trampas comunes¶
- Arreglar cosas en la Fase 39. Tentador; mal. El fix pertenece a la fase originaria. La Fase 39 solo compone.
- 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.
- Comitear datos personales de
mlruns/. Esa es la trampa de supply-chain del §5 #1 del Plan. Añadir a.gitignoreantes del primer commit si no está ya. - Olvidar
--remove-orphansendown. Un contenedor sobrante de una ejecución previa bloquea el siguiente arranque; la idempotencia se rompe. - 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.