4 · Capas, caché y builds multi-etapa
Un build de Docker que tarda 5 minutos puede tardar 5 segundos si entiendes las capas. Ordena tu Dockerfile para que editar código no reinstale dependencias, ignora basura con `.dockerignore`, y envía imágenes ligeras con builds multi-etapa.
Cada instrucción del Dockerfile se convierte en una CAPA cacheada; Docker reutiliza una capa hasta que alguna cambia, entonces la reconstruye junto con todo lo que está debajo. Así que COPIA tus dependencias e instálalas ANTES de copiar tu código que cambia rápido — así una edición de código reutiliza la costosa capa de `pip install` en lugar de rehacerla.
Sin esto:
Equivoca el orden de las capas y cada cambio de una línea en el código dispara un `pip install` completo de scikit-learn, NumPy y compañía — minutos de tiempo de build desperdiciado en cada commit, e imágenes infladas que envían toda tu cadena de herramientas de build.
Una imagen de Docker se construye como una pila de capas — una por instrucción en el Dockerfile. Cada FROM, COPY, RUN produce una nueva capa encima de la anterior. La característica crucial: Docker cachea las capas. En una reconstrucción, baja por el Dockerfile y reutiliza una capa cacheada mientras esa instrucción y sus entradas no hayan cambiado. En el momento en que cambian las entradas de una capa, esa capa y toda capa debajo de ella se reconstruyen desde cero — la caché se "invalida" de ese punto hacia abajo.
Este único hecho dicta cómo ordenar un Dockerfile. El paso costoso es RUN pip install -r requirements.txt (descargar y compilar NumPy, scikit-learn, etc. — a menudo minutos). Tu código, en cambio, cambia constantemente. Así que debes poner el paso lento y que cambia raramente arriba del paso rápido y que cambia a menudo:
COPY requirements.txt . ← cambia raramente
RUN pip install -r requirements.txt ← costoso, pero cacheado si reqs no cambian
COPY . . ← cambia en cada commit
Ahora una edición de una línea en app.py solo invalida la capa final COPY . . — la capa de pip install de arriba se reutiliza de la caché, y la reconstrucción tarda segundos. Si hubieras escrito COPY . . antes de pip install, cada edición de código cambiaría la capa de copia y forzaría una reinstalación completa. Ese ordenamiento es el hábito de rendimiento de Docker más importante.
.dockerignore. COPY . . copia todo el contexto de build. No quieres .git, __pycache__, virtualenvs, datasets ni notebooks en tu imagen — la inflan y, peor, cambiar cualquiera de ellos invalida la caché de la capa de copia. Un archivo .dockerignore (la misma idea que .gitignore) los excluye del contexto de build por completo.
Builds multi-etapa. A veces construir tus dependencias necesita herramientas (compiladores, cabeceras de build) que la app en ejecución no necesita. Un Dockerfile multi-etapa usa una etapa "builder" gorda para compilar/instalar todo, y luego copia solo los artefactos terminados a una etapa "runtime" limpia y ligera. Las herramientas de build se quedan atrás en el builder y nunca se envían. La imagen final es mucho más pequeña — más rápida de subir, bajar y desplegar, y con una superficie de ataque menor.
Un Dockerfile multi-etapa: una etapa `builder` gorda instala las dependencias, luego solo los paquetes terminados se copian a una etapa runtime ligera — la cadena de herramientas de build nunca se envía.
Un `.dockerignore` que excluye `.git`, cachés, virtualenvs, notebooks, datasets y artefactos del contexto de build — imágenes más pequeñas y una caché de capa de copia estable.
En un Dockerfile, ¿por qué copiar `requirements.txt` y ejecutar `pip install` ANTES de `COPY . .` (el resto del código)?
- Cada instrucción del Dockerfile es una capa cacheada; cuando cambia la entrada de una capa, esa capa y toda capa debajo se reconstruyen desde cero.
- Copia + instala las dependencias antes de copiar el código, para que las ediciones de código reutilicen la costosa capa de `pip install`; usa `.dockerignore` para mantener la basura fuera del contexto de build.
- Los builds multi-etapa instalan en una etapa builder gorda y copian solo los paquetes terminados a una etapa runtime ligera — la cadena de herramientas de build nunca se envía, encogiendo la imagen final.
Las imágenes de ML son pesadas (NumPy, scikit-learn, a veces CUDA), así que la caché de capas y el adelgazamiento multi-etapa son lo que mantiene los builds de CI y los despliegues en la nube lo bastante rápidos para iterar sobre un servicio de modelo.
Si lo quitas: Cada commit dispararía una reinstalación completa de dependencias de varios minutos y enviaría una imagen inflada — convirtiendo el bucle de iteración rápida sobre el que se construye este track en uno lento y doloroso.