Módulo 2: Local & Container Deployment
2. Docker Compose para AI Apps
Descripción
En esta cápsula vas a construir un Docker Compose file completo para una app AI multi-container. No un "hello world" con un servicio — un sistema real con FastAPI, Redis, y la estructura que una app AI de producción necesita. Al terminar, tendrás un Compose funcional que puedes adaptar a cualquier proyecto AI.
Contexto: Docker Compose define tu infraestructura como código: qué servicios corren, cómo se comunican, qué datos persisten. Es el "plano" de tu sistema. En esta guía, el Compose que construyes aquí es la base sobre la que se integran LocalStack (M4), migration patterns (M6), y el proyecto integrador (M8).
Compose File: Anatomía Completa
Estructura básica
# docker-compose.yml
# Cada servicio es un container con su configuración
services:
api:
# Tu app FastAPI — el punto de entrada
build: ./api
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REDIS_URL=redis://cache:6379
depends_on:
cache:
condition: service_healthy
cache:
# Redis — cache de responses
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
volumes:
redis_data:
Cada sección explicada
services: # Define los containers que componen tu sistema
api: # Nombre del servicio (también es su hostname en la red interna)
build: ./api # Buildea desde el Dockerfile en ./api/
ports: # Mapeo host:container
environment: # Variables de entorno
depends_on: # Qué servicios necesita antes de arrancar
cache:
image: redis:7-alpine # Usa imagen pre-built (no buildea)
volumes: # Datos persistentes
healthcheck: # Verificación de salud
volumes: # Definición de volúmenes persistentes
redis_data: # Los datos de Redis persisten entre restarts
La App AI: Código Completo
FastAPI con Redis cache
# api/main.py
import hashlib
import json
import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from openai import OpenAI
import redis
app = FastAPI(title="AI API with Cache")
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
cache = redis.Redis.from_url(
os.environ.get("REDIS_URL", "redis://localhost:6379"),
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
@app.get("/health")
def health():
redis_ok = False
try:
redis_ok = cache.ping()
except redis.ConnectionError:
pass
return {
"status": "healthy" if redis_ok else "degraded",
"services": {
"api": "up",
"redis": "up" if redis_ok else "down",
}
}
@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest):
cache_key = hashlib.md5(
f"{request.prompt}:{request.max_tokens}".encode()
).hexdigest()
if request.use_cache:
cached_response = cache.get(cache_key)
if cached_response:
data = json.loads(cached_response)
return AskResponse(answer=data["answer"], cached=True)
try:
response = client.chat.completions.create(
model="gpt-4o-mini",
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
cache.setex(
cache_key,
3600, # TTL: 1 hora
json.dumps({"answer": answer, "tokens": tokens})
)
return AskResponse(answer=answer, cached=False, tokens_used=tokens)
Dockerfile para la API
# api/Dockerfile
FROM python:3.11-slim
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"]
Requirements
# api/requirements.txt
fastapi==0.115.0
uvicorn==0.30.0
openai>=1.0.0
redis==5.0.0
pydantic>=2.0.0
Compose Completo: 3 Servicios
# docker-compose.yml
services:
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REDIS_URL=redis://cache:6379
- ENVIRONMENT=${ENVIRONMENT:-development}
depends_on:
cache:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stopped
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
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:
Levantar y probar
# Levantar servicios principales
docker compose up -d
# Output esperado:
# ✔ Network module-02_default Created
# ✔ Container module-02-cache-1 Healthy
# ✔ Container module-02-api-1 Started
# Verificar health
curl http://localhost:8000/health
# {"status":"healthy","services":{"api":"up","redis":"up"}}
# Test de ask
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "¿Qué es Docker Compose en una línea?"}'
# {"answer":"Docker Compose es...","cached":false,"tokens_used":45}
# Segundo request (cached)
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "¿Qué es Docker Compose en una línea?"}'
# {"answer":"Docker Compose es...","cached":true,"tokens_used":null}
# Levantar con debug tools
docker compose --profile debug up -d
# Ahora redis-commander disponible en http://localhost:8081
Patrones Comunes de Compose para AI
Patrón 1: API + Cache
El más básico y más común. FastAPI + Redis. Cubre el 70% de apps AI.
Patrón 2: API + Cache + Vector Store
Para apps RAG que necesitan búsqueda semántica:
services:
api:
build: ./api
ports:
- "8000:8000"
depends_on:
cache:
condition: service_healthy
vectordb:
condition: service_healthy
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
vectordb:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"]
interval: 10s
volumes:
qdrant_data:
Patrón 3: API + Cache + Worker (async)
Para procesamiento que no cabe en el request/response cycle (documentos largos, batch de embeddings):
services:
api:
build: ./api
ports:
- "8000:8000"
environment:
- REDIS_URL=redis://cache:6379
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
worker:
build: ./worker
environment:
- REDIS_URL=redis://cache:6379
- OPENAI_API_KEY=${OPENAI_API_KEY}
depends_on:
cache:
condition: service_healthy
# El worker lee jobs de Redis y los procesa en background
# La API publica jobs, el worker los consume
Comparación de patrones
| Patrón | Servicios | Cuándo usarlo | Complejidad |
|---|---|---|---|
| API + Cache | 2 | Chatbot, Q&A, asistentes | Baja |
| API + Cache + VectorDB | 3 | RAG, búsqueda semántica | Media |
| API + Cache + Worker | 3 | Procesamiento de documentos, batch | Media |
| API + Cache + VectorDB + Worker | 4+ | Sistema AI completo | Alta |
Recomendación: Empieza con el Patrón 1 (API + Cache). Agrega servicios cuando los necesites, no antes. El Compose file es fácil de extender — más fácil que refactorizar un monolito.
Patrón avanzado: Con Nginx como reverse proxy
Para producción o cuando escalas la API:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
api:
condition: service_healthy
api:
build: ./api
expose:
- "8000" # Solo visible internamente
# ...resto de config
Esto prepara tu Compose para la cápsula 05 (Networking) donde profundizarás en reverse proxy, SSL, y redes aisladas.
Comparación: Docker Run vs Docker Compose
| Aspecto | docker run | docker compose |
|---|---|---|
| Servicios | 1 container | N containers |
| Networking | Manual (--network) | Automático |
| Volúmenes | Manual (-v) | Declarativo |
| Dependencies | No gestionado | depends_on + healthcheck |
| Reproducibilidad | Comandos largos | Un archivo YAML |
| Scaling | Manual | docker compose up --scale api=3 |
Troubleshooting
Problema 1: "El servicio api no conecta a Redis"
# Verificar que Redis está running
docker compose ps
# cache debe estar "Up (healthy)"
# Verificar networking
docker compose exec api ping cache
# Debe resolver a la IP interna de Redis
# Verificar variable de entorno
docker compose exec api env | grep REDIS
# REDIS_URL=redis://cache:6379
Problema 2: "El build tarda demasiado"
# Optimizar Dockerfile con layer caching
# ANTES (rebuild todo al cambiar código):
COPY . .
RUN pip install -r requirements.txt
# DESPUÉS (solo rebuild si requirements cambian):
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
Problema 3: "OpenAI API key no llega al container"
# Verificar que la variable está definida en tu shell
echo $OPENAI_API_KEY
# Si está vacía, Compose pasa una variable vacía al container
# Verifica que .env existe y tiene la variable
cat .env | grep OPENAI
# OPENAI_API_KEY=sk-proj-...
# Verifica dentro del container
docker compose exec api env | grep OPENAI
# Si no aparece, revisa que tu docker-compose.yml tenga:
# environment:
# - OPENAI_API_KEY=${OPENAI_API_KEY}
Problema 4: "Los datos de Redis se pierden al reiniciar"
Asegúrate de tener volume definido y appendonly yes:
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data # Persistencia
volumes:
redis_data: # Debe estar declarado
Problema 5: "Container reinicia en loop (restart: always)"
# Verificar restart count
docker compose ps
# Si ves "Restarting (1)" repetidamente:
# 1. Ver los logs del crash
docker compose logs api --tail=50
# Busca el error que causa el exit
# 2. Temporalmente desactiva restart para ver el error
# Cambia restart: unless-stopped → restart: "no"
# Levanta de nuevo y lee el log de error completo
# 3. Causas comunes en AI apps:
# - OPENAI_API_KEY vacía → la app valida y falla
# - Redis no ready → ConnectionRefusedError en startup
# - Puerto ya ocupado → bind: address already in use
Ejercicios Prácticos
Ejercicio 1: Agrega un servicio de health monitoring
Agrega un servicio que cada 60 segundos haga curl al endpoint /health de la API y loguee el resultado.
Ver solución
health-monitor:
image: alpine/curl
command: >
sh -c "while true; do
echo \"$(date): $(curl -s http://api:8000/health)\";
sleep 60;
done"
depends_on:
api:
condition: service_healthy
Ejercicio 2: Agrega Qdrant como vector store
Extiende el Compose para incluir Qdrant y haz que la API dependa de él.
Ver solución
vectordb:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"]
interval: 10s
timeout: 5s
retries: 3
# Agregar a api:
api:
depends_on:
cache:
condition: service_healthy
vectordb:
condition: service_healthy
environment:
- QDRANT_URL=http://vectordb:6333
# Agregar al final:
volumes:
redis_data:
qdrant_data:
Ejercicio 3: Scaling de la API
Levanta 3 instancias de la API y verifica que todas están healthy.
Ver solución
# Primero, elimina el port mapping fijo (no puedes tener 3 en puerto 8000)
# En docker-compose.yml, cambia ports a expose:
# api:
# expose:
# - "8000"
# # Elimina ports: - "8000:8000"
# Luego escala
docker compose up -d --scale api=3
# Verificar
docker compose ps
# Debe mostrar 3 instancias de api, todas healthy
# Para acceder, necesitas un load balancer (Nginx) adelante
# Eso es avanzado — por ahora, verifica que las 3 arrancan
Ejercicio 4: Docker Compose con profiles
Crea un profile "dev" que incluya redis-commander y hot reload, y un profile "prod" que no los incluya.
Ver solución
services:
api:
build: ./api
# En dev: hot reload con volume mount
volumes:
- ./api:/app # Solo en dev
profiles:
- dev
- prod
api-prod:
build: ./api
# Sin volume mount, sin reload
profiles:
- prod
cache:
image: redis:7-alpine
# Siempre presente (sin profile = todos los profiles)
redis-commander:
image: rediscommander/redis-commander:latest
profiles:
- dev # Solo en dev
docker compose --profile dev up -d # Dev con debug tools
docker compose --profile prod up -d # Prod sin extras
Resumen
- Docker Compose define tu infraestructura como código: servicios, redes, volúmenes.
- Para AI apps, el patrón base es FastAPI + Redis (cache de responses).
depends_onconcondition: service_healthyasegura que los servicios arrancan en orden.- Volumes persisten datos entre restarts (Redis data, vector stores).
- Profiles separan configuración de dev y prod.
- El Compose que construyes aquí es la base para M4, M6, y M8.
Recursos Adicionales
- Docker Compose Specification — Referencia oficial del formato
- Compose Deploy Specification — Config de deployment
- Redis Docker Official Image — Documentación de la imagen Redis
- Qdrant Docker — Qdrant en Docker
- FastAPI with Docker — Deploy de FastAPI en Docker
- Docker Compose Networking — Networking entre servicios