Módulo 14 14 min de lectura

14 - Docker Avanzado para Python

Multi-stage builds, layer caching, security scanning y optimización de imágenes para producción.

#docker #containers #devops #security #optimization

1. El Problema de las Imágenes Python

A diferencia de Node.js donde las imágenes base son ~150MB, una imagen python:3.12 pesa ~1GB. Sin optimización, tu imagen de producción puede llegar a 2GB+.

Imagen BaseTamañoUso
python:3.12~1GB❌ Nunca en prod
python:3.12-slim~150MB✅ Recomendada
python:3.12-alpine~50MB⚠️ Problemas de compilación

Alpine: La Trampa

Alpine usa musl en lugar de glibc. Muchas librerías Python requieren compilación con glibc:

# ❌ Puede tardar 20+ minutos en compilar numpy/pandas
FROM python:3.12-alpine
RUN pip install numpy pandas  # Compila desde source

Recomendación: Usa -slim (Debian) para compatibilidad binaria.


2. Multi-stage Build Óptimo

# ========== STAGE 1: Builder ==========
FROM python:3.12-slim AS builder

# Instalar uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

# Dependencias de compilación (si necesitas)
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copiar SOLO archivos de dependencias (layer caching)
COPY pyproject.toml uv.lock ./

# Instalar dependencias SIN el proyecto
RUN uv sync --frozen --no-dev --no-install-project

# Copiar código fuente
COPY app ./app

# Instalar el proyecto
RUN uv sync --frozen --no-dev

# ========== STAGE 2: Runtime ==========
FROM python:3.12-slim AS runtime

# Usuario no-root
RUN useradd --create-home --uid 1000 appuser

WORKDIR /app

# Copiar SOLO el venv del builder
COPY --from=builder --chown=appuser:appuser /app/.venv ./.venv

# Copiar código (si no está en el venv)
COPY --from=builder --chown=appuser:appuser /app/app ./app

# Configuración
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

USER appuser

EXPOSE 8000

CMD ["gunicorn", "app.main:app", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]

Resultado

StageTamaño
Builder~800MB (se descarta)
Runtime~200MB

3. Layer Caching: El Orden Importa

Docker cachea cada capa. Si una capa cambia, todas las siguientes se invalidan.

# ❌ INCORRECTO: Cualquier cambio en código invalida pip install
COPY . .
RUN pip install -r requirements.txt

# ✅ CORRECTO: Dependencias solo se reinstalan si requirements cambia
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Orden Óptimo de Capas

# 1. Base image (cambia raramente)
FROM python:3.12-slim

# 2. Dependencias del sistema (cambia raramente)
RUN apt-get update && apt-get install -y ...

# 3. Archivos de dependencias Python (cambia ocasionalmente)
COPY pyproject.toml uv.lock ./

# 4. Instalar dependencias (se cachea si pyproject no cambia)
RUN uv sync --frozen

# 5. Código fuente (cambia frecuentemente)
COPY app ./app

4. .dockerignore

Evita copiar archivos innecesarios:

# .dockerignore

# Git
.git
.gitignore

# Python
__pycache__
*.pyc
*.pyo
*.pyd
.Python
.venv
venv
.pytest_cache
.mypy_cache
.ruff_cache

# IDE
.vscode
.idea
*.swp

# Tests
tests
test_*
*_test.py
conftest.py
pytest.ini

# Docs
docs
*.md
!README.md

# CI/CD
.github
.gitlab-ci.yml
Makefile

# Docker
Dockerfile*
docker-compose*
.docker

# Misc
.env*
!.env.example
*.log

5. Seguridad en Imágenes

Usuario No-Root

# Crear usuario
RUN useradd --create-home --uid 1000 appuser

# Cambiar ownership
COPY --chown=appuser:appuser . .

# Ejecutar como usuario
USER appuser

Scanning de Vulnerabilidades

# Trivy (recomendado)
trivy image myapp:latest

# Docker Scout
docker scout cves myapp:latest

# Snyk
snyk container test myapp:latest

GitHub Actions con Scanning

- name: Build image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan for vulnerabilities
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:${{ github.sha }}
    severity: 'CRITICAL,HIGH'
    exit-code: '1'  # Fail si hay vulnerabilidades críticas

Secretos: Nunca en Imagen

# ❌ NUNCA hagas esto
ENV DATABASE_PASSWORD=secret123
COPY .env /app/.env

# ✅ Secretos en runtime
# Se pasan como variables de entorno o secrets de K8s/Docker
CMD ["gunicorn", "app.main:app"]

6. Health Checks

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8000/health || exit 1

Sin curl (imagen más pequeña)

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

7. Docker Compose para Desarrollo

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
    volumes:
      - ./app:/app/app:ro  # Mount código (reload automático)
      - .venv:/app/.venv   # Cache venv
    environment:
      - DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/mydb
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

volumes:
  postgres_data:
  redis_data:

Dockerfile.dev (Con herramientas de desarrollo)

FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen  # Incluye dev dependencies

COPY . .

CMD ["uvicorn", "app.main:app", "--reload", "--host", "0.0.0.0"]

8. Build Cache Avanzado con BuildKit

# Habilitar BuildKit
export DOCKER_BUILDKIT=1

# Build con cache
docker build \
  --cache-from myregistry/myapp:cache \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  -t myapp:latest .

# Push cache para CI
docker push myregistry/myapp:cache

Cache de uv en BuildKit

# syntax=docker/dockerfile:1.4
FROM python:3.12-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

WORKDIR /app
COPY pyproject.toml uv.lock ./

# Mount cache de uv
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --no-install-project

COPY app ./app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

9. Registro de Imágenes

GitHub Container Registry

# .github/workflows/docker.yml
name: Build and Push

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=sha
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

10. Debugging de Imágenes

Inspeccionar Layers

# Ver historial de layers
docker history myapp:latest

# Tamaño de cada layer
docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" myapp:latest

Dive (Visual Layer Explorer)

# Instalar
brew install dive  # macOS
apt install dive   # Linux

# Analizar imagen
dive myapp:latest

Shell en Container

# Entrar en container corriendo
docker exec -it <container_id> /bin/bash

# Entrar en imagen sin CMD
docker run -it --entrypoint /bin/bash myapp:latest

11. Comparativa de Estrategias

EstrategiaTamañoBuild TimeCompatibilidad
python:3.12~1GBRápido✅ Total
python:3.12-slim~150MBRápido✅ Alta
python:3.12-alpine~50MBLento*⚠️ Limitada
Multi-stage slim~200MBMedio✅ Alta
Distroless~100MBMedio⚠️ Sin shell

*Alpine requiere compilar wheels desde source.


12. Checklist de Producción


Conclusión

Docker en Python requiere más atención que en Node.js debido al tamaño de las imágenes y la compilación de dependencias:

  1. Multi-stage es obligatorio para imágenes < 300MB
  2. -slim sobre -alpine para compatibilidad
  3. Layer caching ahorra minutos en CI
  4. Seguridad con usuario no-root y scanning

Pattern Senior: Usa BuildKit cache mounts para acelerar builds en CI. La diferencia entre 5 minutos y 30 segundos de build time es significativa en equipos grandes.

En el siguiente capítulo, configuraremos el despliegue en VPS con Nginx.