2.2 · Optimizar el build : capas, caché y multi-etapa

Nivel 2 · Dockerfile y build

2.2Optimizar el build : capas, caché y multi-etapa

Objetivo: Dividir por 10 el tiempo de build y el tamaño de las imágenes gracias a la caché y al multi-etapa.
Tiempo estimado: 14 min · lectura autónoma · Si solo tienes 5 min: lee «Lo esencial en 30s» + el esquema.

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.

Error frecuente de interpretación: « COPY . . en primera línea, es más simple y no cambia nada ». Falso : el menor cambio invalida la caché desde la primera capa y reejecuta todo, instalación incluida. La regla de Idir : dependencias estables primero (en caché), código cambiante después — y solo se entrega lo ejecutable en multi-etapa.
Un build rápido y ligero Dependencias primero (caché), código después, entrega ligera Dependencias (caché) Estableprimero, install en cachéCódigo (cambiante) Modificado amenudo, reejecutado soloRuntime ligero Multi-etapa,slim, medido
Dependencias primero (caché), código después, entrega ligera
Versión en texto del esquema (SEO / lectores de pantalla / traducción): 1) Idir ORDENA : manifiestos e instalación primero (capas en caché), código después — un cambio de código ya no reejecuta npm install. 2) SEPARA : etapa build equipada para compilar, etapa slim que solo recibe el resultado vía COPY --from=build. 3) MIDE : docker images y docker history antes/después, contexto aligerado por .dockerignore. Lección : el orden da la velocidad, el multi-etapa da el tamaño, la medición prueba ambos.

¿Lo sabías ?

Idir ya ha visto un build reinstalar 400 MB de dependencias por una coma movida en un comentario — COPY . . en primera línea.
Verdadero : una línea mal colocada puede costar minutos en cada build.

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

  1. Compila tu app « ingenua » (todo en un bloque) y anota tamaño + duración.
  2. Reordena : dependencias primero, código después ; recompila tras un cambio de código.
  3. Convierte a multi-etapa (build equipado, runtime slim) y vuelve a medir.
  4. Compara history antes/después y quédate con la receta ganadora.
Tus builds son rápidos y tus imágenes ligeras, cifras en mano.

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.

Lista de salida de la sección
  • 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.
Autoevaluación 1-5 : ¿mi COPY . . viene después del install ?¿mi imagen final contiene algo más que lo ejecutable ?

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.

Comprueba tu comprensión

Para un build rápido, ¿dónde colocar COPY . . (el código) ?

Recursos adicionales (opcional, para ir más allá)

Examen del nivel 2Preguntas inéditas y más difíciles para validar tus conocimientos