Dockerizar aplicaciones Python: Dockerfile optimizado, multi-stage, .dockerignore y Compose

Dockerizar una aplicación Python correctamente va más allá de escribir un FROM python y copiar el código. Un Dockerfile bien optimizado aprovecha la caché de capas de Docker para que las reconstrucciones sean rápidas, produce imágenes pequeñas con --slim y multi-stage builds, y sigue las mejores prácticas de seguridad ejecutando como usuario no-root. Este tutorial cubre cinco ejemplos completos listos para producción.

Imagen base: python:3.13-slim

# Comparativa de tamaños:
# python:3.13           ~1.02 GB
# python:3.13-slim      ~138 MB  — Debian con paquetes mínimos
# python:3.13-alpine    ~53 MB   — Alpine Linux, pero problemas con wheels
# python:3.13-slim-bookworm   — Debian Bookworm (más reciente)

# Recomendación para producción: python:3.13-slim-bookworm

Dockerfile básico con caché de dependencias

# Dockerfile
FROM python:3.13-slim-bookworm

WORKDIR /app

# Copiar solo requirements PRIMERO para cachear esta capa
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && 
    pip install --no-cache-dir -r requirements.txt

# Copiar el código DESPUÉS (esta capa se invalida con cada cambio de código)
COPY . .

# Usuario no-root
RUN adduser --disabled-password --no-create-home appuser
USER appuser

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Multi-stage build: imagen de producción mínima

# Dockerfile con multi-stage
# ?? Etapa 1: Builder ??????????????????????????????????????????????
FROM python:3.13-slim-bookworm AS builder

WORKDIR /build

# Instalar dependencias de compilación solo en el builder
RUN apt-get update && apt-get install -y --no-install-recommends 
    gcc 
    libpq-dev 
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ?? Etapa 2: Runtime ??????????????????????????????????????????????
FROM python:3.13-slim-bookworm AS runtime

# Solo los paquetes de runtime necesarios
RUN apt-get update && apt-get install -y --no-install-recommends 
    libpq5 
    && rm -rf /var/lib/apt/lists/*

# Copiar paquetes Python instalados desde el builder
COPY --from=builder /install /usr/local

WORKDIR /app
COPY . .

# Usuario no-root con UID explícito
RUN groupadd -r appgroup && useradd -r -g appgroup -u 1001 appuser
USER appuser

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

CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

.dockerignore: excluir lo innecesario

# .dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
.env
.env.*
.venv/
venv/
env/
.git/
.gitignore
.pytest_cache/
.mypy_cache/
.ruff_cache/
htmlcov/
*.log
*.sqlite3
tests/
docs/
README.md
Makefile
docker-compose*.yml
.dockerignore

Docker Compose con hot reload para desarrollo

# docker-compose.yml
version: "3.9"

services:
  api:
    build:
      context: .
      target: builder   # etapa de desarrollo con dependencias de compilación
    image: mi-api:dev
    volumes:
      - .:/app          # montar código en tiempo real
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/midb
      - REDIS_URL=redis://redis:6379/0
      - DEBUG=true
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

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

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

Dockerfile para un script CLI

# Para scripts que no son servidores web
FROM python:3.13-slim-bookworm

WORKDIR /app

RUN adduser --disabled-password --no-create-home appuser

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
RUN chown -R appuser:appuser /app

USER appuser

ENTRYPOINT ["python", "mi_script.py"]
CMD ["--help"]   # argumento por defecto; el usuario puede sobreescribirlo

Variables de entorno y secretos

# Nunca hardcodees credenciales en el Dockerfile
# Usa variables de entorno o Docker Secrets

# main.py — acceder a la configuración desde variables de entorno
import os
from functools import lru_cache
from pydantic_settings import BaseSettings

class Configuracion(BaseSettings):
    database_url: str
    redis_url: str = "redis://localhost:6379/0"
    debug: bool = False
    secret_key: str

    class Config:
        env_file = '.env'

@lru_cache
def get_config() -> Configuracion:
    return Configuracion()

# docker run -e SECRET_KEY=xxx -e DATABASE_URL=postgresql://... mi-imagen

Optimizar el tiempo de construcción

  • Ordena las capas: lo que cambia menos frecuente debe ir antes (dependencias ? código ? assets).
  • Combina RUN: cada RUN crea una capa; combínalos con && para reducir tamaño.
  • Limpia en la misma capa: apt-get clean && rm -rf /var/lib/apt/lists/* en el mismo RUN que el apt-get install.
  • pip install sin caché: --no-cache-dir evita guardar el caché de pip dentro de la imagen.
  • BuildKit: activa DOCKER_BUILDKIT=1 para paralelismo y mejor caché.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP