Módulo 2: Local & Container Deployment
6. Debugging Deployment Local
Descripción
En esta cápsula vas a dominar el flujo de debugging cuando algo falla en tu Docker Compose. Saber levantar servicios es la mitad; saber diagnosticar cuando no arrancan o fallan es la otra mitad. Al terminar, tendrás un proceso sistemático para resolver los problemas más comunes en deployment local de apps AI.
Contexto: El estudiante que no sabe debuggear Docker Compose se atasca al primer error y busca en StackOverflow 30 minutos. El que tiene un flujo sistemático resuelve en 5 minutos. Esta cápsula te da ese flujo.
El Flujo de Debugging Sistemático
Los 5 pasos
1. ¿Qué servicio falló? → docker compose ps
2. ¿Qué dicen los logs? → docker compose logs <service>
3. ¿Puedo entrar al container? → docker compose exec <service> sh
4. ¿La red funciona? → ping/curl desde dentro
5. ¿La config es correcta? → docker compose config
Paso 1: Estado de los servicios
docker compose ps
# NAME SERVICE STATUS PORTS
# mod02-api-1 api Up (healthy) 0.0.0.0:8000->8000/tcp
# mod02-cache-1 cache Up (healthy) 6379/tcp
# mod02-worker-1 worker Exited (1)
# El worker se cayó. Siguiente: ver logs.
Presta atención a la columna STATUS. Los estados clave:
| Estado | Significado | Acción |
|---|---|---|
| Up (healthy) | Container corriendo y health check pasando | No action |
| Up (unhealthy) | Container corriendo pero health check falla | Revisar health check endpoint |
| Up | Container corriendo, sin health check | Verificar si debería tener uno |
| Exited (0) | Container terminó normalmente | Revisar si era esperado |
| Exited (1) | Container terminó con error | Ir a Paso 2 (logs) |
| Exited (137) | Container killed (OOM o signal) | Revisar memoria con docker stats |
| Restarting | Container en restart loop | Detener y revisar logs |
Paso 2: Logs
# Logs de un servicio específico
docker compose logs worker
# [ERROR] redis.ConnectionError: Connection refused
# Logs con follow (en tiempo real)
docker compose logs -f api
# Últimas 50 líneas
docker compose logs --tail 50 api
# Logs de todos los servicios
docker compose logs
# Logs con timestamps (útil para correlacionar entre servicios)
docker compose logs -t api cache
# Logs desde un momento específico
docker compose logs --since 5m api
# Últimos 5 minutos
# Logs entre dos momentos
docker compose logs --since 2024-01-15T10:00:00 --until 2024-01-15T10:05:00 api
Paso 3: Exec (entrar al container)
# Shell interactivo
docker compose exec api sh
# o bash si está disponible:
docker compose exec api bash
# Ejecutar un comando puntual
docker compose exec api python -c "import redis; r = redis.Redis.from_url('redis://cache:6379'); print(r.ping())"
# Verificar variables de entorno
docker compose exec api env | grep REDIS
# Verificar que los archivos están donde esperas
docker compose exec api ls -la /app/
# Verificar la versión de Python y paquetes instalados
docker compose exec api python --version
docker compose exec api pip list
# Ejecutar un script de debug rápido
docker compose exec api python -c "
from config import settings
print(f'Environment: {settings.environment}')
print(f'Redis URL: {settings.redis_url}')
print(f'Model: {settings.model_name}')
"
Paso 4: Networking
# Desde dentro del container api, verificar conectividad
docker compose exec api ping -c 3 cache
docker compose exec api curl -s http://localhost:8000/health
# Ver puertos abiertos
docker compose exec api netstat -tlnp 2>/dev/null || ss -tlnp
# Verificar resolución DNS dentro de la red de Compose
docker compose exec api nslookup cache 2>/dev/null || \
docker compose exec api getent hosts cache
# Verificar que Redis responde desde el container de API
docker compose exec api redis-cli -h cache -p 6379 ping
# Listar las redes de Compose
docker network ls | grep module-02
# Inspeccionar qué containers están en la red
docker network inspect module-02_default
Paso 5: Verificar config
# Ver la config mergeada (con variables sustituidas)
docker compose config
# Ver solo un servicio
docker compose config --services
# Verificar que las variables de entorno se resolvieron correctamente
docker compose config | grep -A 5 "environment:"
# Validar que el compose file no tiene errores de sintaxis
docker compose config --quiet && echo "✅ Config válida" || echo "❌ Config inválida"
# Ver la config con un override específico
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
Comparación: logs vs inspect vs exec
Tienes tres herramientas principales para obtener información de un container. Cada una tiene un caso de uso distinto:
| Herramienta | Qué hace | Cuándo usarla |
|---|---|---|
docker compose logs | Ve stdout/stderr del proceso principal | Primer paso siempre: errores de app, stack traces, warnings |
docker inspect | Ve config interna del container (env vars, mounts, redes, estado) | Cuando necesitas ver config de runtime: IPs, variables, restart count |
docker compose exec | Ejecuta comandos dentro del container vivo | Cuando necesitas interactuar: probar conectividad, verificar archivos, correr scripts |
Cuándo usar cada una — ejemplos prácticos
# ESCENARIO: "La API no arranca"
# Paso 1: ¿Hay error en los logs?
docker compose logs --tail 30 api
# → Si ves "ModuleNotFoundError" → falta dependencia
# → Si ves "Connection refused" → dependencia no lista
# ESCENARIO: "La API arranca pero no conecta a Redis"
# Paso 1: ¿Redis está corriendo?
docker compose ps cache
# Paso 2: ¿La URL es correcta?
docker inspect module-02-api-1 --format='{{range .Config.Env}}{{println .}}{{end}}' | grep REDIS
# → REDIS_URL=redis://cache:6379
# Paso 3: ¿Puedo conectar desde dentro?
docker compose exec api redis-cli -h cache ping
# ESCENARIO: "El container se reinicia sin parar"
# Paso 1: ¿Cuántos restarts lleva?
docker inspect module-02-api-1 --format='{{.RestartCount}}'
# → 15 (muchos restarts)
# Paso 2: ¿Qué dice el log antes de morir?
docker compose logs --tail 5 api
# Paso 3: Detener restart para investigar
docker compose stop api
docker inspect — los campos más útiles
# Estado del container (running, exited, restarting)
docker inspect --format='{{.State.Status}}' module-02-api-1
# Exit code del último crash
docker inspect --format='{{.State.ExitCode}}' module-02-api-1
# Cuántos restarts ha tenido
docker inspect --format='{{.RestartCount}}' module-02-api-1
# IP del container en la red de Compose
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' module-02-api-1
# Mounts (volúmenes montados)
docker inspect --format='{{json .Mounts}}' module-02-api-1 | python -m json.tool
# Timestamp del último start
docker inspect --format='{{.State.StartedAt}}' module-02-api-1
Monitoreo de Recursos con docker stats
En apps AI, el monitoreo de recursos es crítico. Un modelo que carga embeddings consume gigabytes. Un request largo a OpenAI puede mantener conexiones abiertas. docker stats es tu dashboard de recursos en tiempo real.
Uso básico
# Monitoreo en tiempo real (actualiza cada segundo)
docker stats
# CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
# mod02-api-1 2.5% 145MiB / 512MiB 28% 1.2kB / 800B 0B / 0B
# mod02-cache-1 0.1% 12MiB / 128MiB 9% 500B / 300B 0B / 4kB
# Snapshot (sin actualización en vivo)
docker stats --no-stream
# Formato personalizado
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}"
# Solo un container
docker stats module-02-api-1
Qué vigilar en apps AI
MÉTRICA NORMAL (AI app) ALERTA CRÍTICO
CPU % 1-20% (idle) >50% sostenido >90%
30-80% (request)
MEM USAGE 100-300MiB >80% del límite Cerca del límite
NET I/O Variable Crecimiento sin Saturación
requests
Script de monitoreo continuo
#!/bin/bash
# monitor.sh — Registra stats cada 5 segundos
LOG_FILE="docker-stats-$(date +%Y%m%d-%H%M%S).csv"
echo "timestamp,container,cpu,mem_usage,mem_limit,mem_pct" > "$LOG_FILE"
while true; do
docker stats --no-stream --format "$(date +%s),{{.Name}},{{.CPUPerc}},{{.MemUsage}},{{.MemPerc}}" \
>> "$LOG_FILE"
sleep 5
done
Establecer límites de memoria en Compose
services:
api:
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
reservations:
memory: 256M
cpus: "0.5"
Cuando un container excede el límite de memoria, Docker lo mata con signal 9 (exit code 137). En apps AI esto es común cuando:
- Cargas un modelo grande en memoria — Si tu app carga embeddings o un modelo local, necesita más RAM
- Acumulas responses sin liberar — Si guardas todas las responses en una lista en memoria
- FastAPI con muchos workers — Cada worker es un proceso separado con su propia memoria
Problemas Comunes en AI Apps
Problema 1: ImportError en la API
api-1 | ImportError: No module named 'openai'
# Causa: requirements.txt no incluye la dependencia
# o el Dockerfile no corre pip install
# Solución: Verificar requirements.txt y Dockerfile
docker compose exec api pip list | grep openai
# Si falta, agregar a requirements.txt y rebuild:
docker compose build api
docker compose up -d
Problema 2: Redis ConnectionError
api-1 | redis.exceptions.ConnectionError: Error while connecting to redis://cache:6379
# Verificar que Redis está running
docker compose ps cache
# Si está "Exited", ver logs:
docker compose logs cache
# Verificar que la URL es correcta
docker compose exec api env | grep REDIS
# Debe ser: REDIS_URL=redis://cache:6379
# Verificar conectividad
docker compose exec api redis-cli -h cache ping
# PONG = OK
Problema 3: Out of Memory (OOM)
api-1 | Killed
# o
api-1 exited with code 137 # 137 = killed by OOM
# Verificar uso de memoria
docker stats
# CONTAINER CPU % MEM USAGE / LIMIT
# api-1 2.5% 450MiB / 512MiB ← Cerca del límite
# Soluciones:
# 1. Aumentar el límite de memoria en compose
# 2. Reducir el uso de memoria de la app
# 3. No cargar modelos grandes en memoria
Problema 4: Timeout de OpenAI API
api-1 | openai.APITimeoutError: Request timed out
# Aumentar timeout en el client
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
timeout=60.0, # Default es 10s, aumentar para prompts largos
max_retries=3,
)
Problema 5: Container se reinicia en loop
docker compose ps
# api-1 Restarting (1) 5 seconds ago
# Ver los últimos logs antes del crash
docker compose logs --tail 20 api
# Buscar el error que causa el crash
# Detener los restarts para investigar
docker compose stop api
docker compose logs api
AI-Specific Debugging: Problemas que Solo Pasan en Apps AI
Las apps AI tienen problemas que no existen en apps web tradicionales. Esta sección cubre los más comunes.
OOM Kills por Carga de Modelos
Cuando tu app carga un modelo local (embeddings, sentence-transformers, etc.), el consumo de memoria salta al arrancar:
# Patrón típico en logs
api-1 | Loading model sentence-transformers/all-MiniLM-L6-v2...
api-1 | Killed
# El container muere antes de terminar de cargar el modelo
docker inspect --format='{{.State.ExitCode}}' module-02-api-1
# 137 = OOM kill
Diagnóstico:
# Ver cuánta memoria necesita tu app al arrancar
docker stats --no-stream
# Antes de cargar modelo: 80MiB
# Después: 450MiB
# Límite: 512MiB → no deja espacio para requests
# Solución 1: Aumentar el límite
# En docker-compose.yml:
# deploy.resources.limits.memory: 1G
# Solución 2: Usar modelos más pequeños
# all-MiniLM-L6-v2 (~80MB) vs all-mpnet-base-v2 (~420MB)
# Solución 3: Lazy loading (cargar en primer request, no al arrancar)
# Lazy loading pattern
class ModelManager:
def __init__(self):
self._model = None
@property
def model(self):
if self._model is None:
from sentence_transformers import SentenceTransformer
self._model = SentenceTransformer("all-MiniLM-L6-v2")
return self._model
model_manager = ModelManager()
LLM API Timeouts y Retries
Los LLMs son lentos comparados con APIs tradicionales. Un request a GPT-4 puede tomar 10-30 segundos:
import time
import logging
from openai import OpenAI, APITimeoutError, RateLimitError
logger = logging.getLogger(__name__)
client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
timeout=60.0,
max_retries=3,
)
def ask_llm_with_retry(prompt: str, max_retries: int = 3) -> str:
for attempt in range(max_retries):
try:
start = time.time()
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
)
elapsed = time.time() - start
logger.info(f"LLM response in {elapsed:.1f}s (attempt {attempt + 1})")
return response.choices[0].message.content
except APITimeoutError:
logger.warning(f"Timeout on attempt {attempt + 1}/{max_retries}")
if attempt == max_retries - 1:
raise
time.sleep(2 ** attempt)
except RateLimitError:
wait = 2 ** (attempt + 2)
logger.warning(f"Rate limited, waiting {wait}s")
time.sleep(wait)
Slow Cold Starts
La primera request después de docker compose up tarda mucho más que las siguientes:
# Medir cold start
time curl -s -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt":"test"}'
# real 0m8.234s ← primera request (cold)
time curl -s -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt":"test"}'
# real 0m0.045s ← segunda request (cached)
Causas y soluciones:
| Causa | Tiempo | Solución |
|---|---|---|
| Uvicorn startup + módulos | 2-5s | Preload modules, --preload flag |
| Primera conexión a Redis | 0.5-1s | start_period en health check |
| Primera request a OpenAI | 3-10s | Warm-up request al arrancar |
| Carga de modelo local | 5-30s | Lazy loading, modelos más pequeños |
# Warm-up pattern: ejecutar al arrancar
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Warm up connections
try:
cache.ping()
logger.info("Redis connection warm")
except Exception:
logger.warning("Redis not available at startup")
yield
app = FastAPI(lifespan=lifespan)
Herramientas de Debugging
docker compose exec vs docker compose run
# exec: entra a un container que YA está corriendo
docker compose exec api sh
# run: crea un nuevo container temporal
docker compose run --rm api python -c "print('test')"
# Diferencia clave:
# exec → mismo container, mismo estado, mismas conexiones
# run → container nuevo, limpio, sin ports mapeados
Inspeccionar un container
# Ver toda la config del container
docker inspect module-02-api-1
# Ver solo las variables de entorno
docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' module-02-api-1
# Ver los mounts
docker inspect --format='{{json .Mounts}}' module-02-api-1 | python -m json.tool
Rebuild limpio
# Si sospechas que el build está corrupto:
docker compose down
docker compose build --no-cache
docker compose up -d
docker compose events — ver eventos en tiempo real
# Monitorear eventos de todos los containers
docker compose events
# Eventos típicos que verás:
# container start, container die, container health_status
# network connect, volume mount
# Útil para debug de restart loops:
docker compose events --filter event=die
# Te muestra cada vez que un container muere
Logs estructurados con JSON
Si configuras tu app para loguear en JSON, puedes filtrar más fácilmente:
import logging
import json
class JSONFormatter(logging.Formatter):
def format(self, record):
log_entry = {
"timestamp": self.formatTime(record),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
}
if record.exc_info:
log_entry["exception"] = self.formatException(record.exc_info)
return json.dumps(log_entry)
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger = logging.getLogger("ai-api")
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
# Filtrar logs JSON por nivel de error
docker compose logs api 2>&1 | python -c "
import sys, json
for line in sys.stdin:
try:
entry = json.loads(line.split('| ', 1)[-1])
if entry.get('level') == 'ERROR':
print(json.dumps(entry, indent=2))
except (json.JSONDecodeError, IndexError):
pass
"
Ejercicios Prácticos
Ejercicio 1: Diagnostica un fallo simulado
Detén Redis mientras la API está corriendo. ¿Qué pasa? ¿Cómo lo diagnosticas?
Ver solución
# Detener Redis
docker compose stop cache
# Verificar estado
docker compose ps
# cache: Exited, api: Up (pero ¿healthy?)
# Hacer un request
curl http://localhost:8000/ask -X POST \
-H "Content-Type: application/json" \
-d '{"prompt":"test"}'
# Si implementaste graceful degradation: funciona sin cache
# Si no: retorna error de conexión
# Verificar health
curl http://localhost:8000/health
# {"status":"degraded","services":{"api":"up","redis":"down"}}
# Restaurar
docker compose start cache
Ejercicio 2: Debug de memory leak
Usa docker stats para monitorear el uso de memoria de tu API durante 10 requests. ¿Crece la memoria?
Ver solución
# Terminal 1: monitorear stats
docker stats module-02-api-1
# Terminal 2: enviar requests
for i in $(seq 1 10); do
curl -s -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt":"Test request '$i'"}' > /dev/null
echo "Request $i sent"
done
# Observar en Terminal 1 si MEM USAGE crece significativamente
# Si crece y no baja: posible memory leak
# Si se estabiliza: normal (caching, object pools)
Ejercicio 3: Script de diagnóstico
Crea un script debug.sh que ejecute los 5 pasos de debugging automáticamente.
Ver solución
#!/bin/bash
echo "=== 1. Service Status ==="
docker compose ps
echo -e "\n=== 2. Recent Logs (last 10 lines per service) ==="
for service in $(docker compose config --services); do
echo "--- $service ---"
docker compose logs --tail 10 "$service" 2>&1
done
echo -e "\n=== 3. Health Checks ==="
curl -s http://localhost:8000/health | python -m json.tool 2>/dev/null || echo "API not responding"
echo -e "\n=== 4. Resource Usage ==="
docker stats --no-stream
echo -e "\n=== 5. Config Validation ==="
docker compose config --quiet && echo "Config OK" || echo "Config ERROR"
Ejercicio 4: Diagnostica un exit code 137
Configura un límite de memoria muy bajo (64MB) para el servicio api y haz un request que cargue mucho en memoria. Diagnostica el crash y corrígelo.
Ver solución
# docker-compose.debug.yml — override con memoria muy baja
services:
api:
deploy:
resources:
limits:
memory: 64M
# Levantar con el override restrictivo
docker compose -f docker-compose.yml -f docker-compose.debug.yml up -d
# Verificar que arrancó
docker compose ps
# api Up (healthy) — puede que sí arranque con 64MB
# Hacer un request pesado
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt":"Escribe un ensayo extenso sobre la historia de la inteligencia artificial desde los años 50 hasta hoy", "max_tokens": 2000}'
# Verificar si murió
docker compose ps
# Si STATUS es "Exited (137)" → OOM kill
# Confirmar con inspect
docker inspect --format='{{.State.ExitCode}}' module-02-api-1
# 137
# Verificar cuánta memoria estaba usando antes del kill
docker compose logs --tail 5 api
# Puede que no haya log (killed abruptamente)
# Solución: aumentar el límite
# Cambiar 64M → 512M en el override
# O quitar el override: docker compose up -d (usa config base)
# Verificar la corrección
docker compose up -d
docker stats --no-stream
# MEM USAGE debería tener margen suficiente
Ejercicio 5: Correlación de logs entre servicios
Tu API reporta "Redis connection error" pero Redis dice que está UP. Usa logs con timestamps para diagnosticar el problema real.
Ver solución
# Ver logs con timestamps de ambos servicios
docker compose logs -t api cache 2>&1 | sort -t 'Z' -k1
# Ejemplo de output:
# 2024-01-15T10:00:01Z cache-1 | Ready to accept connections
# 2024-01-15T10:00:01Z api-1 | Starting uvicorn...
# 2024-01-15T10:00:02Z api-1 | redis.ConnectionError: Connection refused
# 2024-01-15T10:00:05Z cache-1 | Ready to accept connections on port 6379
# El problema: api intentó conectar ANTES de que Redis estuviera listo
# Aunque Redis "arrancó", no estaba aceptando conexiones aún
# Solución: depends_on con condition: service_healthy
# (Ya implementado en nuestro compose, pero si no lo tuvieras)
# Verificar que el health check de Redis funciona
docker compose exec cache redis-cli ping
# PONG
# Verificar que depends_on está bien configurado
docker compose config | grep -A 5 "depends_on"
# depends_on:
# cache:
# condition: service_healthy
# Si el timing sigue siendo un problema, agregar retry en la app:
import time
import redis
def get_redis_connection(url: str, max_retries: int = 5) -> redis.Redis:
for attempt in range(max_retries):
try:
r = redis.Redis.from_url(url, decode_responses=True)
r.ping()
return r
except redis.ConnectionError:
if attempt < max_retries - 1:
wait = 2 ** attempt
print(f"Redis not ready, retrying in {wait}s...")
time.sleep(wait)
else:
raise
Troubleshooting
"docker compose logs no muestra nada"
Si un container muere inmediatamente, puede que no haya logs. Usa docker compose run para ver el error:
docker compose run --rm api python -c "from config import settings; print('OK')"
Si el error es en el import de settings (como una variable faltante), lo verás aquí.
"Container Exited (2) — syntax error"
Exit code 2 suele ser error de bash/shell. Verifica el CMD/ENTRYPOINT en tu Dockerfile:
docker compose logs api
# /bin/sh: uvicorn: not found
# Solución: pip install no se ejecutó, rebuild:
docker compose build --no-cache api
"Port already in use"
# Error: bind: address already in use
# Alguien más está usando el puerto 8000
# Encontrar qué proceso usa el puerto
lsof -i :8000
# o
netstat -tlnp | grep 8000
# Solución: cambiar el puerto en .env o matar el proceso
# En .env: API_PORT=8001
"Container no puede resolver hostname de otro servicio"
# Error: Could not resolve host: cache
# Los containers no están en la misma red
docker network ls | grep module-02
# Si no hay red → los containers no están conectados
# Solución: verificar que ambos servicios están en el mismo compose file
docker compose config --services
# Debe listar: api, cache
"Health check siempre falla pero la app funciona"
# El health check usa curl, pero curl no está instalado en la imagen
docker compose exec api curl --version
# sh: curl: not found
# Solución: instalar curl en el Dockerfile
# RUN apt-get update && apt-get install -y curl
# O usar un health check que no requiera curl:
# test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
Resumen
- Debugging sistemático en 5 pasos: ps → logs → exec → networking → config.
- Los problemas más comunes en AI apps: ImportError, ConnectionError, OOM, timeout de LLM APIs, restart loops.
docker compose logs -f <service>es tu herramienta principal.docker compose exec <service> shte da acceso directo al container.docker statsmonitorea CPU y memoria en tiempo real — crítico para apps AI con modelos pesados.docker inspectrevela config interna: env vars, IPs, restart count, exit codes.- OOM kills (exit code 137) son el problema #1 en apps AI — monitorea memoria siempre.
- LLM timeouts necesitan retry con backoff exponencial — los LLMs son lentos.
- Rebuild limpio (
build --no-cache) cuando sospechas corrupción del build.
Recursos Adicionales
- Docker Compose CLI Reference — Todos los comandos de Compose
- Docker Logs — Configuración de logging
- Docker Stats — Monitoreo de recursos
- Debugging Docker Containers — Métricas y debugging
- FastAPI Debugging — Tips de debugging en FastAPI
- Python Logging Best Practices — Configurar logging en Python
- Docker Events — Monitoreo de eventos en tiempo real