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
RUNcrea una capa; combínalos con&¶ reducir tamaño. - Limpia en la misma capa:
apt-get clean && rm -rf /var/lib/apt/lists/*en el mismoRUNque elapt-get install. - pip install sin caché:
--no-cache-direvita guardar el caché de pip dentro de la imagen. - BuildKit: activa
DOCKER_BUILDKIT=1para paralelismo y mejor caché.
