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:
- Levanta una app AI multi-container con
docker compose up - Incluye FastAPI (API), Redis (cache), y health checks
- Tiene configuración por entorno (dev, staging, prod)
- Maneja secrets de forma segura
- Cachea responses de LLM para reducir costes
- Se puede debuggear con el flujo sistemático del módulo
Recap del Módulo
| Cápsula | Concepto | Lo usas en el proyecto |
|---|---|---|
| 02 | Docker Compose multi-service | Estructura base del proyecto |
| 03 | Environment configuration | .env files, overrides |
| 04 | Health checks y dependencies | Readiness verificada |
| 05 | Networking y comunicación | Servicios conectados |
| 06 | Debugging | Flujo de diagnóstico |
| 07 | Secrets y seguridad | API keys protegidas |
Especificaciones Técnicas
Arquitectura
┌─────────────┐
HTTP :8000 ───→ │ FastAPI │
│ (api) │
└──────┬──────┘
│
┌──────┴──────┐
│ │
┌─────┴─────┐ ┌────┴─────┐
│ Redis │ │ OpenAI │
│ (cache) │ │ API │
└───────────┘ └──────────┘
Servicios requeridos
| Servicio | Imagen/Build | Puerto | Health check |
|---|---|---|---|
| api | Build desde ./api | 8000 (host) | GET /health |
| cache | redis:7-alpine | 6379 (interno) | redis-cli ping |
| redis-commander | rediscommander/redis-commander | 8081 (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_onconcondition: service_healthy) - El endpoint
/askdebe intentar cache primero, llamar a OpenAI solo si no hay cache hit - Si Redis está caído,
/askdebe 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
maxmemoryy políticaallkeys-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.examplecon placeholders para onboardingdocker-compose.override.ymlpara desarrollo (hot reload, debug logging)docker-compose.prod.ymlpara producción (multiple workers, warning logging, resource limits)- Pydantic Settings valida que
OPENAI_API_KEYexiste y no es placeholder
Debugging:
- Script
debug.shque 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)
| Criterio | Puntos | Cómo se evalúa |
|---|---|---|
docker compose up levanta todos los servicios sin errores | 8 | Ejecutar y verificar docker compose ps |
/health retorna status correcto de api y redis | 6 | curl /health muestra ambos servicios |
/health/detailed muestra latencia de dependencias | 4 | curl /health/detailed incluye latency_ms |
/ask invoca LLM y retorna respuesta | 8 | POST con prompt retorna answer |
Cache funciona: segundo request idéntico retorna cached: true | 8 | Repetir request, verificar campo cached |
/cache/stats muestra hit rate y memoria | 3 | curl /cache/stats retorna métricas |
DELETE /cache limpia el cache | 3 | DELETE + repetir request = cached: false |
Configuración (25 puntos)
| Criterio | Puntos | Cómo se evalúa |
|---|---|---|
.env no está en Git; .env.example sí | 5 | git status, verificar .gitignore |
.dockerignore excluye .env y archivos sensibles | 3 | cat .dockerignore |
Pydantic Settings valida OPENAI_API_KEY | 5 | Arrancar sin key → error claro |
docker-compose.override.yml con hot reload | 4 | Editar código → se refleja sin rebuild |
docker-compose.prod.yml con workers y resource limits | 4 | docker compose -f ... config muestra workers |
| Variables de entorno configurables (LOG_LEVEL, CACHE_TTL, etc.) | 4 | Cambiar en .env, restart, verificar comportamiento |
Debugging (20 puntos)
| Criterio | Puntos | Cómo se evalúa |
|---|---|---|
debug.sh ejecuta los 5 pasos de diagnóstico | 8 | bash debug.sh produce output útil |
| Health checks funcionales (api y cache) | 4 | docker compose ps muestra (healthy) |
| Graceful degradation: app funciona sin Redis | 4 | docker compose stop cache, luego /ask sigue funcionando |
| redis-commander accesible con profile debug | 4 | docker compose --profile debug up -d, abrir :8081 |
Documentación (15 puntos)
| Criterio | Puntos | Cómo se evalúa |
|---|---|---|
.env.example completo con todas las variables | 5 | Comparar variables en código vs .env.example |
| Instrucciones claras para setup (README o comentarios) | 5 | Alguien nuevo puede levantar el proyecto siguiendo instrucciones |
| Código comentado donde no es obvio (config, health checks) | 5 | Leer código, entender sin explicación externa |
Escala de calificación
| Rango | Calificación |
|---|---|
| 90-100 | Excelente — listo para producción |
| 75-89 | Bueno — funcional con mejoras menores |
| 60-74 | Aceptable — funciona pero falta robustez |
| < 60 | Necesita 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 uplevanta todos los servicios sin errores -
/healthretorna status de api y redis -
/health/detailedmuestra latencia de cada dependencia -
/askinvoca LLM y retorna respuesta - Segundo request idéntico retorna
cached: true -
/cache/statsmuestra hit rate y memoria -
DELETE /cachelimpia el cache correctamente
Configuración
-
.envno está en Git;.env.examplesí -
.dockerignoreexcluye.envy archivos sensibles - Config Settings valida que OPENAI_API_KEY existe
- Health checks verifican readiness real
- Compose funciona con override (dev) y prod
Debugging
-
debug.shcorre sin errores y produce output útil - redis-commander accesible con
--profile debug - App funciona en modo degradado sin Redis
Documentación
-
.env.exampletiene 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.ymlpara dev,prod.ymlpara 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.shejecuta 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
- Docker Compose File Reference — Especificación completa
- FastAPI Production Deployment — Guía oficial de deployment
- Redis Configuration — Configuración de Redis
- Uvicorn Deployment — Opciones de deployment de uvicorn
- Docker Best Practices — Best practices de Dockerfiles
- Pydantic Settings — Validación de configuración
- OpenAI Python SDK — Cliente oficial de OpenAI