Un Dockerfile es la receta que convierte tu código fuente de Python en una imagen de contenedor: un paquete de solo lectura, organizado en capas, que contiene el espacio de usuario de un sistema operativo, el runtime de Python, tus dependencias y tu código. Todos los servicios de contenedores de Azure en este examen (App Service, Container Apps, Container Apps jobs y AKS, el Azure Kubernetes Service) ejecutan imágenes construidas de esta forma, así que un Dockerfile limpio es la primera habilidad sobre la que se construye el resto del dominio.
Empieza con la imagen base en la línea FROM. Las imágenes oficiales de Python vienen en variantes: la imagen completa basada en Debian es grande pero trae herramientas de compilación; las imágenes -slim eliminan la mayoría de esas herramientas y son la opción habitual para producción; las imágenes Alpine son diminutas pero usan musl en lugar de glibc, por lo que muchos wheels de Python deben compilarse desde el código fuente, lo que en la práctica suele hacer las compilaciones más lentas y las imágenes más grandes. Fija una etiqueta de versión específica (por ejemplo, una versión menor de Python más -slim) en lugar de latest, para que las recompilaciones sean predecibles.
Cada instrucción (RUN, COPY, ADD) crea una capa, y Docker almacena las capas en caché. Ordena las instrucciones de la que cambia con menos frecuencia a la que cambia con más: copia primero requirements.txt, ejecuta pip install --no-cache-dir -r requirements.txt y solo después copia el resto de tu código. Así, un cambio de código reutiliza la capa de dependencias en caché en lugar de reinstalarlo todo.
Una compilación multietapa (multi-stage build) usa más de un FROM. La primera etapa (el builder) tiene compiladores y cabeceras e instala los wheels en un entorno virtual; la etapa final parte de una imagen de runtime pequeña y usa COPY --from=builder para traer solo el entorno virtual terminado y tu código. Las herramientas de compilación, las cachés y los secretos usados durante la compilación nunca llegan a la imagen final, lo que reduce el tamaño y la superficie de ataque.
FROM python:3.12-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
RUN useradd --create-home appuser
COPY --from=builder /opt/venv /opt/venv
ENV PATH=/opt/venv/bin:$PATH
WORKDIR /app
COPY . .
USER appuser
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
De forma predeterminada, el proceso de un contenedor se ejecuta como root. Si un atacante explota tu app, ser root dentro del contenedor facilita escapar de él o manipularlo. Crea un usuario sin privilegios y cambia a él con USER antes de CMD, como se muestra arriba. Escucha en un puerto superior a 1024, porque los usuarios que no son root no pueden enlazar puertos bajos sin capacidades adicionales.
Por último, .dockerignore funciona como .gitignore para el contexto de compilación: enumera archivos que nunca se envían al builder. Excluye .git, __pycache__, los entornos virtuales, los datos de prueba y, sobre todo, los archivos .env o las credenciales locales. Esto acelera las compilaciones (en especial las remotas que suben el contexto, como az acr build) y mantiene los secretos fuera de las capas de COPY . ., donde cualquiera que descargue la imagen podría leerlos.
Términos clave
- Base image (imagen base)
- La imagen indicada en FROM sobre la que se construye tu imagen, como una imagen slim de Python.
- Multi-stage build (compilación multietapa)
- Un Dockerfile con varias etapas FROM en el que la etapa final copia solo los artefactos necesarios de las etapas anteriores.
- Build context (contexto de compilación)
- El conjunto de archivos que se envía al builder con una compilación; .dockerignore quita archivos de él.
- Layer cache (caché de capas)
- La reutilización que hace Docker de los resultados de instrucciones sin cambios, lo que hace que el orden de las instrucciones importe para la velocidad de compilación.
- USER instruction (instrucción USER)
- Define el usuario que usan las instrucciones posteriores y el contenedor en ejecución, para que la app no tenga que ejecutarse como root.
La imagen de FastAPI de un equipo pesaba 1.1 GB y recompilaba todas las dependencias en cada commit. Cambiaron a una base slim, copiaron requirements.txt antes que el código, agregaron una etapa builder para los wheels compilados y un .dockerignore que excluía .git y .env. La imagen final bajó a unos cientos de megabytes, las recompilaciones tras cambios solo de código tardaban segundos y un escaneo de seguridad dejó de señalar un archivo .env filtrado.
Comprueba lo que sabes
¿Por qué deberías copiar requirements.txt y ejecutar pip install antes de copiar el resto del código fuente?
Porque las capas se almacenan en caché en orden; así, los cambios de código reutilizan la capa de dependencias en caché en lugar de reinstalar cada paquete.
¿Qué mejora una compilación multietapa y cómo lo logra?
El tamaño y la seguridad de la imagen: las herramientas de compilación viven solo en una etapa anterior, y la etapa final usa COPY --from para tomar solo los artefactos compilados.
¿Por qué ejecutar la app con un USER distinto de root?
Para limitar lo que puede hacer un atacante si la app se ve comprometida; ser root en el contenedor facilita la manipulación y los intentos de escape.