mlops · temario
5.2tema 2 de 5

Docker & Deploy

Contenedores ligeros, variables de entorno, healthcheck y despliegue reproducible.

Docker resuelve el problema clásico de 'funciona en mi máquina': empaqueta tu aplicación junto con todas sus dependencias en una imagen que se ejecuta de forma idéntica en cualquier entorno. Para un modelo de ML esto es especialmente valioso porque las versiones de numpy, scikit-learn o torch importan enormemente: una diferencia de versión menor puede cambiar los resultados de predicción.

Una imagen de Docker se construye en capas definidas por el Dockerfile. Cada instrucción (FROM, RUN, COPY) genera una capa que Docker cachea. Si cambias una línea, solo se reconstruyen las capas desde esa instrucción hacia abajo. El orden importa: copia primero los archivos de dependencias (requirements.txt o pyproject.toml) e instálalas, y luego copia el código fuente. Así, cuando cambias solo el código, la instalación de paquetes se toma del cache.

Las imágenes base ligeras son críticas en producción. python:3.11 pesa más de 1 GB; python:3.11-slim pesa ~200 MB; python:3.11-slim + solo las dependencias necesarias puede quedar en ~400 MB con el modelo incluido. Imágenes más pequeñas arrancan más rápido, se transfieren más rápido al registry, y tienen una superficie de ataque menor (menos paquetes del sistema operativo = menos CVEs potenciales).

Las variables de entorno son la forma estándar de configurar un contenedor sin reconstruir la imagen. Nunca escribas credenciales, URLs de base de datos o API keys directamente en el Dockerfile o en el código: las imágenes se guardan en registries y su historial de capas es inspeccionable. Usa ENV en el Dockerfile para valores por defecto no sensibles, e inyecta los valores sensibles en tiempo de ejecución con -e o un archivo .env que nunca se comitea.

El healthcheck dentro del Dockerfile (o en docker-compose) le dice al orquestador si el contenedor está listo para recibir tráfico. Sin healthcheck, Docker marca el contenedor como 'running' desde que arranca el proceso, aunque la API todavía esté cargando el modelo (que puede tardar 10-30 segundos). Un balanceador de carga que enruta tráfico a un contenedor no-listo produce errores 502/503 en producción.

Un flujo de CI/CD completo construye la imagen, la etiqueta con el SHA del commit, la empuja al registry (Docker Hub, ECR, GCR) y luego actualiza el servicio en producción. Etiquetar con el SHA en lugar de 'latest' es crítico: 'latest' es mutable y no permite saber qué código está corriendo realmente. Con el SHA puedes hacer rollback exacto en segundos.

structure.txt
# Dockerfile — producción optimizado
FROM python:3.11-slim

# Non-root user for security
RUN adduser --disabled-password --gecos '' appuser

WORKDIR /app

# Layer cache: install deps first, code second
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY src/ ./src/
COPY model.pkl .

# Non-sensitive defaults via ENV
ENV PORT=8000 \
    LOG_LEVEL=info \
    WORKERS=1

# Healthcheck — wait 10s before first check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:${PORT}/health').raise_for_status()"

USER appuser
EXPOSE ${PORT}

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

# .dockerignore
.git
.venv
__pycache__
*.pyc
.env
tests/
notebooks/

# Build & run
docker build -t churn-api:$(git rev-parse --short HEAD) .
docker run -p 8000:8000 -e MODEL_VERSION=v2 churn-api:<sha>

# docker-compose.yml
services:
  api:
    image: churn-api:latest
    ports: ['8000:8000']
    environment:
      - MODEL_VERSION=${MODEL_VERSION}
    healthcheck:
      test: ['CMD', 'python', '-c', "import httpx; httpx.get('http://localhost:8000/health').raise_for_status()"]
      interval: 30s
      start_period: 10s

Debugging lab

Detecta y corrige el error en el código.

0/5 tests passing0%
  1. 5.2.5.1

    FROM python:3.11 WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

  2. 5.2.5.2

    FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

  3. 5.2.5.3

    FROM python:3.11-slim ENV DATABASE_URL=postgresql://admin:supersecret123@prod-db:5432/churn ENV API_KEY=sk-live-abc123xyz WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["uvicorn", "main:app"]

  4. 5.2.5.4

    docker build -t churn-api:latest . docker push registry.io/churn-api:latest

  5. 5.2.5.5

    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"] # .dockerignore no existe