2.2Optimizar el build : capas, caché y multi-etapa
Lo esencial en 30 segundos
- El orden dirige la caché : Docker reejecuta las capas hasta la primera modificada — lo estable primero, lo cambiante después.
- Multi-etapa = construir grande, entregar ligero : se compila en una etapa equipada, solo se entrega lo ejecutable — COPY --from=build.
- Medir antes de optimizar : docker images y docker history prueban la ganancia — apuntar por debajo de 200 MB para una app web.
Mini-caso hilado — Idir y el build que lo reinstalaba todo
La app empaquetada de Idir (2.1) funciona, pero cada coma modificada relanza npm install y el build dura minutos ; la imagen supera el gigabyte. Idir reordena : dependencias estables primero (package*.json + install), código cambiante después — un cambio de código ya no reejecuta la instalación. Luego pasa a multi-etapa : una etapa equipada compila, una etapa slim solo recibe el resultado vía COPY --from=build. Veredicto cifrado : de 1 GB a unos 150 MB. En 2.3, Idir etiquetará esta imagen ligera ; en 2.5, la reforzará.
Continuación del hilo conductor 2.1 : la app de Idir se vuelve rápida de compilar y ligera de entregar — y continúa en 2.3, 2.4 y 2.5.
1. La caché : el orden lo cambia todo
Docker reejecuta las capas en orden y reutiliza la caché hasta la primera capa modificada : todo lo siguiente se reejecuta. Por eso se coloca primero lo estable (las dependencias : COPY package*.json y luego install) y después lo cambiante (el código : COPY . .). Invertir el orden vuelve a descargar todo con cada coma modificada. Idir copia los manifiestos e instala ANTES del código : un cambio de código solo reejecuta las últimas capas.
2. El multi-etapa : construir grande, entregar ligero
Se compila en una imagen equipada (etapa « build » con SDK completo) y luego se copia solo el resultado en una imagen de ejecución ligera (COPY --from=build). El SDK, las fuentes y las cachés se quedan en el andén : la imagen final solo contiene lo necesario para funcionar. Cada FROM abre una etapa ; solo se entrega la última. Idir construye en imagen completa y entrega en node:20-slim.
3. Medir antes de optimizar
docker images da el tamaño, docker history el peso por capa : se optimiza lo que se mide. Objetivo realista : una app web por debajo de 200 MB (base slim + multi-etapa). Un .dockerignore completo (node_modules, .git, dist) aligera el contexto enviado al daemon. Por debajo del umbral, se cazan las dependencias inútiles, no los bytes. Idir anota tamaño y duración antes/después : cifras en mano, nada de intuición.
¿Lo sabías ?
El orden de las líneas es tiempo de vida.
🔎 Ejemplo concreto — 🔎 Ejemplo concreto — Node : de 1 GB a 150 MB
El Dockerfile de Idir, versión optimizada y medida :
# stage 1 : construction
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# stage 2 : exécution maigre
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "dist/server.js"]
Lectura : el SDK y las fuentes se quedan en la etapa build, abandonada después ; la etapa final solo contiene lo ejecutable.
Lección : docker history prueba qué capas se han fundido — medir antes de aplaudir.
Vocabulario de la sección
- Caché de build
- La reutilización de las capas sin cambios entre dos builds : todo se reejecuta a partir de la primera capa modificada.
Ejemplo sencillo: Idir ya no reejecuta npm install cuando solo cambia su código.
Lo que no es: una imagen final más pequeña.
Reflejo: estable primero para usar la caché.
- Multi-stage
- Varias instrucciones FROM : se construye en una etapa equipada y solo se entrega la última etapa, ligera.
Ejemplo sencillo: Idir compila en node:20 y luego entrega en node:20-slim.
Lo que no es: un simple reordenamiento de las capas.
Reflejo: construir grande, entregar ligero.
- COPY --from
- Copiar desde una etapa anterior (no desde el contexto) : solo se trae el resultado compilado.
Ejemplo sencillo: Idir copia /app/dist desde la etapa build hacia la etapa slim.
Lo que no es: un COPY clásico desde el contexto.
Reflejo: entregar solo lo ejecutable.
- history
- El detalle capa por capa de una imagen (docker history) : peso y origen de cada capa.
Ejemplo sencillo: Idir compara history antes/después para probar la ganancia.
Lo que no es: el tamaño total solo (docker images).
Reflejo: medir por capa antes de optimizar.
En la práctica — En la práctica — Fundir una imagen
- Compila tu app « ingenua » (todo en un bloque) y anota tamaño + duración.
- Reordena : dependencias primero, código después ; recompila tras un cambio de código.
- Convierte a multi-etapa (build equipado, runtime slim) y vuelve a medir.
- Compara history antes/después y quédate con la receta ganadora.
Script listo para decir — justificar la optimización (30 s)
« Antes, cada modificación relanzaba toda la instalación y la imagen superaba el gigabyte. Ahora las dependencias están en caché, solo se reejecuta el código, y el multi-etapa solo entrega lo ejecutable : unos 150 MB. La prueba : docker images e history antes/después. Más rápido de compilar, más ligero de desplegar. »
Para decir mostrando las dos mediciones de Idir, cifras en mano.
Modelo copiar-pegar — Dockerfile multi-etapa Node
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "dist/server.js"]
Caché muerta
COPY . . en primera línea : cada build lo reinstala todo — Idir sube las dependencias antes del código.
SDK entregado
Imagen de prod con el SDK completo : Idir pasa a multi-etapa y solo entrega el dist.
Contexto pesado
Contexto de 800 MB : Idir escribe un .dockerignore (node_modules, .git, dist) y el build respira.
Optimizar a ciegas
« Es más ligero, lo intuyo » : Idir saca docker history y prueba la ganancia capa por capa.
✓ Buenas prácticas
- Dependencias primero, código después, siempre.
- Un .dockerignore completo (node_modules, .git, dist).
- Entregar en slim, construir en completa.
✗ Errores frecuentes
- COPY . . en primera línea (caché muerta).
- Entregar el SDK en producción.
- Optimizar sin medir (history/images).
Puntos clave
- El orden de las capas dirige la caché (estable primero, cambiante después).
- Multi-etapa : solo se entrega lo ejecutable (COPY --from=build).
- Se mide : docker images (tamaño) + docker history (por capa).
- .dockerignore completo : el contexto no debe transportar lo inútil.
Desafío 48h — aplica mañana
En tu app empaquetada (2.1) : anota tamaño + duración, reordena dependencias y luego código, convierte a multi-etapa, y luego compara history antes/después.
- Puedo ordenar un Dockerfile para la caché (estable y luego cambiante).
- Sé escribir un build multi-etapa que solo entregue lo ejecutable.
- Mido con images + history antes de anunciar una ganancia.
Preguntas frecuentes
¿Por qué mi build reejecuta npm install cada vez ?
Porque COPY . . precede a RUN npm ci : el menor archivo modificado invalida la caché desde esa capa. Copia package*.json e instala ANTES del código : solo un cambio de dependencias reejecutará la instalación.
¿El multi-etapa ralentiza el build ?
Un poco (dos etapas por construir), pero divide el tamaño entregado y la superficie de ataque. En CI con caché, el sobrecoste es marginal frente a la ganancia en despliegue y seguridad.
Para un build rápido, ¿dónde colocar COPY . . (el código) ?