Mejora tus Dockerfile
Introducción
Los Dockerfile son como las recetas de cocina: puedes seguir una receta básica y obtener un resultado aceptable, o puedes optimizar cada paso para obtener un plato estrella. La mayoría de los Dockerfile que veo en proyectos reales son funcionales, pero están lejos de ser óptimos.
Un Dockerfile bien optimizado significa: builds más rápidos, imágenes más pequeñas, menos vulnerabilidades, y despliegues más seguros. No son optimizaciones triviales: una reducción de 1GB a 200MB en el tamaño de la imagen puede suponer un ahorro significativo en costes de almacenamiento y ancho de banda.
En este artículo voy a compartir las técnicas que uso para optimizar Dockerfile en proyectos Python, basadas en años de experiencia en producción.
Fundamentos: capas y caché
Cada instrucción en un Dockerfile crea una capa. Docker cachea cada capa y solo reconstruye las que cambian. Aprovechar este mecanismo es la clave para builds rápidos.
# Malo: todo en una capa
RUN apt-get update && apt-get install -y build-essential
RUN pip install -r requirements.txt
# Bueno: agrupar comandos relacionados
RUN apt-get update && apt-get install -y build-essential \
&& rm -rf /var/lib/apt/lists/*
Orden de las instrucciones
Coloca las instrucciones que cambian menos frecuentemente al principio. Así aprovechas mejor la caché:
# 1. Sistema base (rara vez cambia)
FROM python:3.12-slim
# 2. Dependencias del sistema (cambia poco)
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
# 3. Dependencias Python (cambia con requirements.txt)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 4. Código fuente (cambia constantemente)
COPY . .
Multi-stage builds
La técnica más importante para reducir el tamaño de las imágenes es usar multi-stage builds. Consiste en usar múltiples etapas donde las primeras contienen herramientas de compilación y la última solo lo necesario para ejecutar:
# Etapa 1: Compilación
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y build-essential curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# Etapa 2: Ejecución
FROM python:3.12-slim
COPY --from=builder /wheels /wheels
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
COPY . .
CMD ["python", "app.py"]
Imágenes base mínimas
Elegir la imagen base correcta puede reducir drásticamente el tamaño:
# 1.1 GB (full Python)
FROM python:3.12
# 160 MB (slim)
FROM python:3.12-slim
# 50 MB (Alpine Linux, requiere compilación)
FROM python:3.12-alpine
Para la mayoría de proyectos, slim es el mejor balance entre tamaño y compatibilidad. Alpine puede dar problemas con algunas librerías que requieren compilación.
Reducir el número de capas
Cada capa ocupa espacio. Aunque las capas se compartan, es buena práctica minimizar su número:
# Malo
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2
RUN rm -rf /var/lib/apt/lists/*
# Bueno
RUN apt-get update && apt-get install -y package1 package2 \
&& rm -rf /var/lib/apt/lists/*
Caché de dependencias Python
Las dependencias Python cambian menos frecuentemente que el código. Copia requirements.txt antes que el código para aprovechar la caché:
# Malo: copiar todo el código antes de instalar dependencias
COPY . .
RUN pip install -r requirements.txt
# Bueno: copiar requirements primero
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
No ejecutar como root
Por seguridad, ejecuta la aplicación con un usuario no privilegiado:
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# O en versiones modernas de Python
FROM python:3.12-slim
RUN addgroup --system app && adduser --system --group app
USER app
Exponer solo lo necesario
No expongas puertos que no sean necesarios:
EXPOSE 8000
# No: EXPOSE 22 8000 9000
Usar .dockerignore
Un buen .dockerignore evita que archivos innecesarios se copien al contexto de build:
.git/
__pycache__/
*.pyc
.env
.venv/
venv/
node_modules/
dist/
*.md
tests/
HEALTHCHECK
Añade un healthcheck para que Docker pueda monitorizar la aplicación:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
Labels
Añade metadatos a la imagen:
LABEL org.opencontainers.image.source="https://github.com/user/repo"
LABEL org.opencontainers.image.description="API de mi aplicación"
LABEL org.opencontainers.image.licenses="MIT"
Ejemplo completo optimizado
# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential curl \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim
RUN addgroup --system app && adduser --system --group app
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
COPY . .
RUN mkdir -p /app/static && chown -R app:app /app
USER app
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
LABEL org.opencontainers.image.source="https://github.com/user/repo"
LABEL org.opencontainers.image.description="API optimizada"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Análisis de tamaño
Para identificar qué ocupa espacio en tu imagen:
docker history --human --no-trunc mi-imagen
docker system df
docker build --no-cache -t test .
Usa dive para una inspección visual:
dive mi-imagen
Buenas prácticas de seguridad
- No almacenar secrets en el build: Usa
--secretde Docker BuildKit si necesitas credenciales. - Escanea vulnerabilidades:
docker scoutotrivy. - Imágenes base oficiales: Usa imágenes oficiales y mantenidas.
- Actualiza regularmente: Las imágenes base contienen vulnerabilidades conocidas.
- Mínimo privilegio: Usa
USERno root,chmodrestrictivos.
Conclusión
Optimizar un Dockerfile no es difícil, pero requiere conocer las técnicas adecuadas. Multi-stage builds, orden de capas, imágenes base slim, y ejecución no root son las prácticas más impactantes.
Mi recomendación: empieza por medir. Antes de optimizar, ejecuta docker images y dive para entender qué ocupa espacio en tu imagen actual. Luego aplica las optimizaciones una por una, midiendo el impacto de cada cambio.
Un Dockerfile bien optimizado no solo ahorra espacio y tiempo de build, sino que también mejora la seguridad y la mantenibilidad del proyecto. Es una inversión que se amortiza rápidamente.