14 - Docker Avanzado para Python
Multi-stage builds, layer caching, security scanning y optimización de imágenes para producción.
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 Base | Tamaño | Uso |
|---|---|---|
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
| Stage | Tamañ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
| Estrategia | Tamaño | Build Time | Compatibilidad |
|---|---|---|---|
python:3.12 | ~1GB | Rápido | ✅ Total |
python:3.12-slim | ~150MB | Rápido | ✅ Alta |
python:3.12-alpine | ~50MB | Lento* | ⚠️ Limitada |
| Multi-stage slim | ~200MB | Medio | ✅ Alta |
| Distroless | ~100MB | Medio | ⚠️ Sin shell |
*Alpine requiere compilar wheels desde source.
12. Checklist de Producción
- Multi-stage build para separar builder y runtime
- Usuario no-root (
USER appuser) - Layer ordering óptimo para caching
- .dockerignore completo
- Healthcheck configurado
- Sin secretos en la imagen
- Scanning de vulnerabilidades en CI
- Tags inmutables (SHA o semver, nunca solo
latest)
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:
- Multi-stage es obligatorio para imágenes < 300MB
-slimsobre-alpinepara compatibilidad- Layer caching ahorra minutos en CI
- 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.