Módulo 2: Local & Container Deployment

8. Proyecto: Local Multi-Container AI App

Descripción del proyecto

Este es el proyecto integrador del Módulo 2. Vas a construir una app AI multi-container completa con Docker Compose: FastAPI como API gateway, Redis como cache de responses, configuración por entorno, health checks funcionales, y un flujo de debugging documentado. El resultado es un sistema local que replica producción.

Por qué importa: Este Compose es el artefacto base para el resto de la guía. En el Módulo 4, le agregarás LocalStack como servicio. En el Módulo 6, será tu entorno de desarrollo mientras migras a AWS. Y en el Módulo 8, será el punto de partida del pipeline Docker → CI/CD → plataforma.


Objetivo del proyecto

Producir un Docker Compose funcional que:

  1. Levanta una app AI multi-container con docker compose up
  2. Incluye FastAPI (API), Redis (cache), y health checks
  3. Tiene configuración por entorno (dev, staging, prod)
  4. Maneja secrets de forma segura
  5. Cachea responses de LLM para reducir costes
  6. Se puede debuggear con el flujo sistemático del módulo

Recap del Módulo

CápsulaConceptoLo usas en el proyecto
02Docker Compose multi-serviceEstructura base del proyecto
03Environment configuration.env files, overrides
04Health checks y dependenciesReadiness verificada
05Networking y comunicaciónServicios conectados
06DebuggingFlujo de diagnóstico
07Secrets y seguridadAPI keys protegidas

Especificaciones Técnicas

Arquitectura

                    ┌─────────────┐
    HTTP :8000 ───→ │   FastAPI    │
                    │   (api)      │
                    └──────┬──────┘
                           │
                    ┌──────┴──────┐
                    │             │
              ┌─────┴─────┐ ┌────┴─────┐
              │   Redis    │ │  OpenAI  │
              │  (cache)   │ │   API    │
              └───────────┘ └──────────┘

Servicios requeridos

ServicioImagen/BuildPuertoHealth check
apiBuild desde ./api8000 (host)GET /health
cacheredis:7-alpine6379 (interno)redis-cli ping
redis-commanderrediscommander/redis-commander8081 (debug only)profile: debug

Endpoints requeridos

GET  /health              → Status de todos los servicios
GET  /health/detailed     → Latencia de cada dependencia
POST /ask                 → Pregunta al LLM (con cache)
GET  /cache/stats         → Hit rate del cache
DELETE /cache             → Limpiar cache

Requisitos detallados

API (FastAPI):

  • Debe arrancar solo cuando Redis esté healthy (depends_on con condition: service_healthy)
  • El endpoint /ask debe intentar cache primero, llamar a OpenAI solo si no hay cache hit
  • Si Redis está caído, /ask debe seguir funcionando (graceful degradation) — llama a OpenAI directo
  • El health check debe reportar status individual de cada dependencia
  • Los errores de OpenAI API deben retornar HTTP 502 con mensaje descriptivo
  • Logging configurado por variable de entorno (LOG_LEVEL)

Cache (Redis):

  • Configurado con maxmemory y política allkeys-lru (para que no crezca sin límite)
  • Datos persistidos con appendonly yes
  • TTL configurable via variable de entorno (CACHE_TTL)

Configuración:

  • Todas las variables sensibles en .env (nunca en código)
  • .env.example con placeholders para onboarding
  • docker-compose.override.yml para desarrollo (hot reload, debug logging)
  • docker-compose.prod.yml para producción (multiple workers, warning logging, resource limits)
  • Pydantic Settings valida que OPENAI_API_KEY existe y no es placeholder

Debugging:

  • Script debug.sh que ejecuta diagnóstico de los 5 pasos
  • redis-commander disponible con profile debug

Archivos requeridos

module-02-project/
├── api/
│   ├── main.py                  # App FastAPI completa
│   ├── config.py                # Settings con Pydantic
│   ├── requirements.txt         # Dependencias
│   └── Dockerfile               # Multi-stage o slim
├── docker-compose.yml           # Base config
├── docker-compose.override.yml  # Dev overrides (hot reload)
├── docker-compose.prod.yml      # Production overrides
├── .env                         # Variables dev (NO en Git)
├── .env.example                 # Template (SÍ en Git)
├── .gitignore                   # Ignora .env y secrets
├── .dockerignore                # Ignora .env en build
└── debug.sh                     # Script de diagnóstico

Rúbrica de Evaluación (100 puntos)

Funcionalidad (40 puntos)

CriterioPuntosCómo se evalúa
docker compose up levanta todos los servicios sin errores8Ejecutar y verificar docker compose ps
/health retorna status correcto de api y redis6curl /health muestra ambos servicios
/health/detailed muestra latencia de dependencias4curl /health/detailed incluye latency_ms
/ask invoca LLM y retorna respuesta8POST con prompt retorna answer
Cache funciona: segundo request idéntico retorna cached: true8Repetir request, verificar campo cached
/cache/stats muestra hit rate y memoria3curl /cache/stats retorna métricas
DELETE /cache limpia el cache3DELETE + repetir request = cached: false

Configuración (25 puntos)

CriterioPuntosCómo se evalúa
.env no está en Git; .env.example5git status, verificar .gitignore
.dockerignore excluye .env y archivos sensibles3cat .dockerignore
Pydantic Settings valida OPENAI_API_KEY5Arrancar sin key → error claro
docker-compose.override.yml con hot reload4Editar código → se refleja sin rebuild
docker-compose.prod.yml con workers y resource limits4docker compose -f ... config muestra workers
Variables de entorno configurables (LOG_LEVEL, CACHE_TTL, etc.)4Cambiar en .env, restart, verificar comportamiento

Debugging (20 puntos)

CriterioPuntosCómo se evalúa
debug.sh ejecuta los 5 pasos de diagnóstico8bash debug.sh produce output útil
Health checks funcionales (api y cache)4docker compose ps muestra (healthy)
Graceful degradation: app funciona sin Redis4docker compose stop cache, luego /ask sigue funcionando
redis-commander accesible con profile debug4docker compose --profile debug up -d, abrir :8081

Documentación (15 puntos)

CriterioPuntosCómo se evalúa
.env.example completo con todas las variables5Comparar variables en código vs .env.example
Instrucciones claras para setup (README o comentarios)5Alguien nuevo puede levantar el proyecto siguiendo instrucciones
Código comentado donde no es obvio (config, health checks)5Leer código, entender sin explicación externa

Escala de calificación

RangoCalificación
90-100Excelente — listo para producción
75-89Bueno — funcional con mejoras menores
60-74Aceptable — funciona pero falta robustez
< 60Necesita trabajo — revisar cápsulas del módulo

Ejemplo de Implementación Mínima

Este es el ejemplo mínimo que pasa la rúbrica (≥60 puntos). Tu implementación debería ser mejor que esto.

api/config.py (mínimo)

import os
from pydantic_settings import BaseSettings
from pydantic import field_validator

class Settings(BaseSettings):
    openai_api_key: str
    redis_url: str = "redis://cache:6379"
    environment: str = "development"
    log_level: str = "debug"
    cache_ttl: int = 3600
    model_name: str = "gpt-4o-mini"
    max_tokens: int = 500
    
    @field_validator("openai_api_key")
    @classmethod
    def validate_key(cls, v):
        if not v or "replace" in v.lower():
            raise ValueError("Set a real OPENAI_API_KEY in .env")
        return v
    
    class Config:
        env_file = ".env"

settings = Settings()

api/main.py (mínimo)

import hashlib
import json
import logging
import time
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openai import OpenAI
import redis

from config import settings

logging.basicConfig(level=getattr(logging, settings.log_level.upper()))
logger = logging.getLogger(__name__)

app = FastAPI(title="AI API", version="1.0.0")
client = OpenAI(api_key=settings.openai_api_key, timeout=30.0)
cache = redis.Redis.from_url(settings.redis_url, decode_responses=True)

class AskRequest(BaseModel):
    prompt: str
    max_tokens: int = 500
    use_cache: bool = True

class AskResponse(BaseModel):
    answer: str
    cached: bool
    tokens_used: int | None = None
    model: str = ""

@app.get("/health")
def health():
    checks = {"api": "up"}
    try:
        cache.ping()
        checks["redis"] = "up"
    except Exception:
        checks["redis"] = "down"
    
    overall = "healthy" if all(v == "up" for v in checks.values()) else "degraded"
    status_code = 200 if overall == "healthy" else 503
    return JSONResponse(
        status_code=status_code,
        content={"status": overall, "environment": settings.environment, "services": checks}
    )

@app.get("/health/detailed")
def health_detailed():
    checks = {}
    
    start = time.time()
    try:
        cache.ping()
        checks["redis"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["redis"] = {"status": "down", "error": str(e)}
    
    start = time.time()
    try:
        client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": "ping"}],
            max_tokens=5
        )
        checks["openai"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["openai"] = {"status": "down", "error": str(e)}
    
    overall = "healthy" if all(c.get("status") == "up" for c in checks.values()) else "degraded"
    return {"status": overall, "checks": checks}

@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest):
    cache_key = f"ask:{hashlib.md5(f'{request.prompt}:{request.max_tokens}'.encode()).hexdigest()}"
    
    if request.use_cache:
        try:
            cached = cache.get(cache_key)
            if cached:
                data = json.loads(cached)
                return AskResponse(answer=data["answer"], cached=True, model=data.get("model", ""))
        except redis.ConnectionError:
            logger.warning("Redis unavailable, proceeding without cache")
    
    try:
        response = client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": request.prompt}],
            max_tokens=request.max_tokens,
        )
    except Exception as e:
        raise HTTPException(status_code=502, detail=f"LLM API error: {str(e)}")
    
    answer = response.choices[0].message.content
    tokens = response.usage.total_tokens
    
    try:
        cache.setex(
            cache_key, settings.cache_ttl,
            json.dumps({"answer": answer, "tokens": tokens, "model": settings.model_name})
        )
    except redis.ConnectionError:
        logger.warning("Redis unavailable, response not cached")
    
    return AskResponse(answer=answer, cached=False, tokens_used=tokens, model=settings.model_name)

@app.get("/cache/stats")
def cache_stats():
    try:
        info = cache.info()
        return {
            "hits": info.get("keyspace_hits", 0),
            "misses": info.get("keyspace_misses", 0),
            "hit_rate": round(
                info.get("keyspace_hits", 0) / max(1, info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0)) * 100, 1
            ),
            "keys": cache.dbsize(),
            "memory_used": info.get("used_memory_human", "unknown"),
        }
    except redis.ConnectionError:
        return {"error": "Redis unavailable"}

@app.delete("/cache")
def clear_cache():
    try:
        cache.flushdb()
        return {"status": "cache cleared"}
    except redis.ConnectionError:
        raise HTTPException(status_code=503, detail="Redis unavailable")

api/requirements.txt

fastapi==0.115.0
uvicorn==0.30.0
openai>=1.0.0
redis==5.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0

api/Dockerfile

FROM python:3.11-slim

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Código Completo (Implementación de Referencia)

Esta implementación de referencia incluye todo lo necesario para obtener 90+ puntos.

api/config.py

import os
from pydantic_settings import BaseSettings
from pydantic import field_validator

class Settings(BaseSettings):
    openai_api_key: str
    redis_url: str = "redis://cache:6379"
    environment: str = "development"
    log_level: str = "debug"
    cache_ttl: int = 3600
    model_name: str = "gpt-4o-mini"
    max_tokens: int = 500
    
    @field_validator("openai_api_key")
    @classmethod
    def validate_key(cls, v):
        if not v or "replace" in v.lower():
            raise ValueError("Set a real OPENAI_API_KEY in .env")
        return v
    
    @field_validator("log_level")
    @classmethod
    def validate_log_level(cls, v):
        valid = {"debug", "info", "warning", "error", "critical"}
        if v.lower() not in valid:
            raise ValueError(f"LOG_LEVEL must be one of: {valid}")
        return v.lower()
    
    @property
    def is_dev(self) -> bool:
        return self.environment == "development"
    
    class Config:
        env_file = ".env"

settings = Settings()

api/main.py

import hashlib
import json
import logging
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openai import OpenAI
import redis

from config import settings

logging.basicConfig(level=getattr(logging, settings.log_level.upper()))
logger = logging.getLogger(__name__)

client = OpenAI(api_key=settings.openai_api_key, timeout=30.0, max_retries=2)
cache = redis.Redis.from_url(settings.redis_url, decode_responses=True)

@asynccontextmanager
async def lifespan(app: FastAPI):
    try:
        cache.ping()
        logger.info("Redis connection established")
    except redis.ConnectionError:
        logger.warning("Redis not available at startup — will retry on requests")
    yield

app = FastAPI(title="AI API", version="1.0.0", lifespan=lifespan)

class AskRequest(BaseModel):
    prompt: str
    max_tokens: int = 500
    use_cache: bool = True

class AskResponse(BaseModel):
    answer: str
    cached: bool
    tokens_used: int | None = None
    model: str = ""

@app.get("/health")
def health():
    checks = {"api": "up"}
    try:
        cache.ping()
        checks["redis"] = "up"
    except Exception:
        checks["redis"] = "down"
    
    overall = "healthy" if all(v == "up" for v in checks.values()) else "degraded"
    status_code = 200 if overall == "healthy" else 503
    return JSONResponse(
        status_code=status_code,
        content={"status": overall, "environment": settings.environment, "services": checks}
    )

@app.get("/health/detailed")
def health_detailed():
    checks = {}
    
    start = time.time()
    try:
        cache.ping()
        checks["redis"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["redis"] = {"status": "down", "error": str(e)}
    
    start = time.time()
    try:
        r = client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": "ping"}],
            max_tokens=5
        )
        checks["openai"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["openai"] = {"status": "down", "error": str(e)}
    
    overall = "healthy" if all(c.get("status") == "up" for c in checks.values()) else "degraded"
    return {"status": overall, "checks": checks}

@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest):
    cache_key = f"ask:{hashlib.md5(f'{request.prompt}:{request.max_tokens}'.encode()).hexdigest()}"
    
    if request.use_cache:
        try:
            cached = cache.get(cache_key)
            if cached:
                data = json.loads(cached)
                logger.debug(f"Cache hit for key {cache_key[:8]}")
                return AskResponse(answer=data["answer"], cached=True, model=data.get("model", ""))
        except redis.ConnectionError:
            logger.warning("Redis unavailable, proceeding without cache")
    
    try:
        response = client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": request.prompt}],
            max_tokens=request.max_tokens,
        )
    except Exception as e:
        logger.error(f"OpenAI API error: {e}")
        raise HTTPException(status_code=502, detail=f"LLM API error: {str(e)}")
    
    answer = response.choices[0].message.content
    tokens = response.usage.total_tokens
    
    try:
        cache.setex(
            cache_key, settings.cache_ttl,
            json.dumps({"answer": answer, "tokens": tokens, "model": settings.model_name})
        )
    except redis.ConnectionError:
        logger.warning("Redis unavailable, response not cached")
    
    return AskResponse(answer=answer, cached=False, tokens_used=tokens, model=settings.model_name)

@app.get("/cache/stats")
def cache_stats():
    try:
        info = cache.info()
        return {
            "hits": info.get("keyspace_hits", 0),
            "misses": info.get("keyspace_misses", 0),
            "hit_rate": round(
                info.get("keyspace_hits", 0) / max(1, info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0)) * 100, 1
            ),
            "keys": cache.dbsize(),
            "memory_used": info.get("used_memory_human", "unknown"),
        }
    except redis.ConnectionError:
        return {"error": "Redis unavailable"}

@app.delete("/cache")
def clear_cache():
    try:
        cache.flushdb()
        return {"status": "cache cleared"}
    except redis.ConnectionError:
        raise HTTPException(status_code=503, detail="Redis unavailable")

api/requirements.txt

fastapi==0.115.0
uvicorn==0.30.0
openai>=1.0.0
redis==5.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0

api/Dockerfile

FROM python:3.11-slim

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml

services:
  api:
    build:
      context: ./api
    ports:
      - "${API_PORT:-8000}:8000"
    env_file:
      - .env
    environment:
      - REDIS_URL=redis://cache:6379
    depends_on:
      cache:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped

  redis-commander:
    image: rediscommander/redis-commander:latest
    environment:
      - REDIS_HOSTS=local:cache:6379
    ports:
      - "8081:8081"
    depends_on:
      cache:
        condition: service_healthy
    profiles:
      - debug

volumes:
  redis_data:

docker-compose.override.yml (dev)

services:
  api:
    volumes:
      - ./api:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    environment:
      - LOG_LEVEL=debug

docker-compose.prod.yml

services:
  api:
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
    environment:
      - LOG_LEVEL=warning
      - ENVIRONMENT=production
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M

.env.example

OPENAI_API_KEY=sk-proj-replace-with-your-key
ENVIRONMENT=development
LOG_LEVEL=debug
CACHE_TTL=3600
MODEL_NAME=gpt-4o-mini
MAX_TOKENS=500
API_PORT=8000

.gitignore

.env
.env.production
.env.staging
!.env.example
secrets/
__pycache__/
*.pyc

.dockerignore

.env
.env.*
!.env.example
secrets/
.git/
__pycache__/
*.pyc
.gitignore
debug.sh
docker-compose*.yml

debug.sh

#!/bin/bash
echo "=== Service Status ==="
docker compose ps

echo -e "\n=== Health Check ==="
curl -s http://localhost:8000/health | python3 -m json.tool 2>/dev/null || echo "API not responding"

echo -e "\n=== Detailed Health ==="
curl -s http://localhost:8000/health/detailed | python3 -m json.tool 2>/dev/null || echo "Detailed health unavailable"

echo -e "\n=== Cache Stats ==="
curl -s http://localhost:8000/cache/stats | python3 -m json.tool 2>/dev/null || echo "Stats unavailable"

echo -e "\n=== Resource Usage ==="
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

echo -e "\n=== Recent Logs (errors only) ==="
docker compose logs --tail 20 2>&1 | grep -i "error\|exception\|failed" || echo "No errors found"

echo -e "\n=== Config Validation ==="
docker compose config --quiet && echo "✅ Config OK" || echo "❌ Config ERROR"

Paso a Paso para Construir

1. Crear estructura (2 min)

mkdir -p module-02-project/api
cd module-02-project

2. Crear archivos de código (10 min)

Copia los archivos de las secciones anteriores: config.py, main.py, requirements.txt, Dockerfile.

3. Crear Docker Compose files (5 min)

Copia docker-compose.yml, docker-compose.override.yml, docker-compose.prod.yml.

4. Crear archivos de seguridad (2 min)

Copia .gitignore, .dockerignore, .env.example.

5. Configurar environment (2 min)

cp .env.example .env
# Edita .env con tu OPENAI_API_KEY real

6. Levantar y probar (5 min)

docker compose up -d
# Esperar a que los health checks pasen

# Test health
curl http://localhost:8000/health

# Test ask
curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt": "¿Qué es Docker Compose?"}'

# Test cache (repetir el mismo request)
curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt": "¿Qué es Docker Compose?"}'
# Debe retornar cached: true

# Cache stats
curl http://localhost:8000/cache/stats

7. Probar configuración por entorno (3 min)

# Production mode
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
curl http://localhost:8000/health
# environment: "production" si configuraste .env.production

8. Probar debugging (3 min)

chmod +x debug.sh
bash debug.sh

# Probar redis-commander
docker compose --profile debug up -d
# Abrir http://localhost:8081 en el browser

9. Probar graceful degradation (3 min)

# Detener Redis
docker compose stop cache

# Verificar que la API sigue respondiendo
curl http://localhost:8000/health
# {"status":"degraded","services":{"api":"up","redis":"down"}}

curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt": "test sin cache"}'
# Debe funcionar (sin cache, directo a OpenAI)

# Restaurar Redis
docker compose start cache

Checklist de Completitud

Funcionalidad

  • docker compose up levanta todos los servicios sin errores
  • /health retorna status de api y redis
  • /health/detailed muestra latencia de cada dependencia
  • /ask invoca LLM y retorna respuesta
  • Segundo request idéntico retorna cached: true
  • /cache/stats muestra hit rate y memoria
  • DELETE /cache limpia el cache correctamente

Configuración

  • .env no está en Git; .env.example
  • .dockerignore excluye .env y archivos sensibles
  • Config Settings valida que OPENAI_API_KEY existe
  • Health checks verifican readiness real
  • Compose funciona con override (dev) y prod

Debugging

  • debug.sh corre sin errores y produce output útil
  • redis-commander accesible con --profile debug
  • App funciona en modo degradado sin Redis

Documentación

  • .env.example tiene todas las variables necesarias
  • Código tiene comments donde no es obvio

Troubleshooting del Proyecto

"docker compose up falla con 'openai_api_key is required'"

Tu .env no tiene OPENAI_API_KEY o es un placeholder. Copia .env.example a .env y llena con tu key real.

cp .env.example .env
# Edita .env: OPENAI_API_KEY=sk-proj-tu-key-real
docker compose up -d

"El primer request es muy lento"

Normal en dev con hot reload — uvicorn recarga módulos al primer request. En prod (sin --reload), es más rápido. El cold start de la API es independiente del cold start de Lambda.

# Verificar tiempo de respuesta
time curl -s -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"ping"}'
# Primera request: 3-10s (normal, incluye cold start de OpenAI)
# Siguientes requests (cached): <100ms

"redis-commander no aparece"

Necesitas el profile debug: docker compose --profile debug up -d

# Verificar que el container está corriendo
docker compose --profile debug ps
# redis-commander debe estar Up

# Verificar el puerto
curl -s http://localhost:8081 | head -5
# Debe retornar HTML

"Cache no funciona — siempre retorna cached: false"

# Verificar que Redis está healthy
docker compose ps cache
# Debe ser: Up (healthy)

# Verificar conectividad desde la API
docker compose exec api python -c "
import redis
r = redis.Redis.from_url('redis://cache:6379', decode_responses=True)
r.set('test', 'hello')
print(r.get('test'))  # Debe imprimir: hello
"

# Verificar que CACHE_TTL no es 0
docker compose exec api env | grep CACHE_TTL

"Exit code 137 al hacer requests pesados"

Tu container se está quedando sin memoria. Aumenta el límite o reduce el uso:

# Ver uso actual de memoria
docker stats --no-stream

# Si estás usando docker-compose.prod.yml con memory: 512M
# y tu app necesita más, aumenta:
# deploy.resources.limits.memory: 1G

# O corre sin limits para dev:
docker compose up -d  # usa override.yml sin limits

"docker compose build tarda mucho"

El primer build descarga la imagen base y las dependencias. Los siguientes son más rápidos gracias al cache de capas:

# Si necesitas forzar un rebuild limpio:
docker compose build --no-cache api

# Tip: separa COPY requirements.txt y pip install ANTES de COPY . .
# Así el pip install solo se re-ejecuta si requirements.txt cambió

Conexión con la Guía

Este Docker Compose es tu artefacto base. En los próximos módulos:

  • M3 (Lambda): Aprenderás la alternativa serverless
  • M4 (LocalStack): Agregarás LocalStack como servicio en este Compose
  • M6 (Migration): Este Compose será tu entorno de desarrollo
  • M8 (Integrador): Este Compose es el punto de partida del deploy a producción

Resumen

  • El proyecto integra todas las cápsulas del Módulo 2: Compose, env config, health checks, networking, debugging y secrets.
  • La arquitectura es FastAPI + Redis + OpenAI API — el stack base para apps AI con cache.
  • Graceful degradation es clave: si Redis cae, la app sigue funcionando (sin cache).
  • La configuración por entorno (override.yml para dev, prod.yml para producción) refleja cómo se trabaja en equipos reales.
  • Pydantic Settings valida secrets al arrancar — nunca arranca con configuración inválida.
  • El script debug.sh ejecuta los 5 pasos de diagnóstico en un comando.
  • Este Compose es tu artefacto base para el resto de la guía — lo vas a reutilizar en los módulos 4, 6 y 8.

Recursos para el Proyecto

  1. Docker Compose File Reference — Especificación completa
  2. FastAPI Production Deployment — Guía oficial de deployment
  3. Redis Configuration — Configuración de Redis
  4. Uvicorn Deployment — Opciones de deployment de uvicorn
  5. Docker Best Practices — Best practices de Dockerfiles
  6. Pydantic Settings — Validación de configuración
  7. OpenAI Python SDK — Cliente oficial de OpenAI