Optimización de Capas y Caché de Build

Lectura
20 min~5 min lectura

Concepto clave

La optimización de capas y el uso del caché de build son fundamentales para crear imágenes Docker eficientes. Cada instrucción en un Dockerfile genera una capa, y Docker cachea cada capa para acelerar builds posteriores. Piensa en las capas como capas de una cebolla: cada capa se apila sobre la anterior, y si una capa cambia, todas las capas posteriores deben reconstruirse. El objetivo es minimizar las reconstrucciones colocando las instrucciones que cambian con frecuencia (como copiar código fuente) al final del Dockerfile, y las que cambian raramente (como instalar dependencias) al principio. Esto maximiza el uso del caché y reduce el tiempo de build.

Además, es crucial reducir el número de capas combinando instrucciones relacionadas (por ejemplo, usando && para encadenar comandos en un solo RUN). También debes limpiar archivos temporales en la misma capa para evitar que ocupen espacio en la imagen final. Una imagen optimizada no solo es más rápida de construir, sino también más pequeña y segura.

Cómo funciona en la práctica

Imagina que tienes una aplicación Node.js. Un Dockerfile típico podría verse así:

FROM node:18
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "index.js"]

Aquí, las capas son: FROM, WORKDIR, COPY package.json..., RUN npm install, COPY . .. Si cambias el código fuente (capa COPY . .), Docker reutilizará las capas anteriores desde el caché, siempre que no hayan cambiado. Pero si cambias package.json, la capa COPY package.json... se invalida y RUN npm install se reconstruye, lo cual es correcto porque las dependencias pueden haber cambiado.

Para optimizar aún más, puedes usar un .dockerignore para evitar copiar archivos innecesarios (como node_modules) y así reducir el contexto de build. También puedes usar imágenes base más pequeñas, como node:18-alpine.

Código en acción

Veamos un antes y después. Primero, un Dockerfile no optimizado:

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
RUN rm -rf node_modules
CMD ["node", "dist/index.js"]

Problemas: cada RUN es una capa separada, y rm -rf node_modules no reduce el tamaño porque los archivos ya están en una capa anterior. Además, COPY . . invalida el caché cada vez que cualquier archivo cambia.

Versión optimizada:

FROM node:18-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Aquí usamos un build multi-etapa: la primera etapa construye la app, la segunda solo copia los artefactos necesarios. Esto reduce drásticamente el tamaño de la imagen final. Además, separamos COPY package.json... de COPY . . para aprovechar el caché de dependencias.

Para verificar el tamaño, ejecuta docker image ls y compara. Una buena práctica es usar docker history para inspeccionar las capas.

Errores comunes

  1. No usar .dockerignore: Copiar node_modules o archivos temporales al contexto de build aumenta el tiempo de build y el tamaño de la imagen. Solución: crear un archivo .dockerignore con node_modules, .git, etc.
  2. Instalar dependencias después de copiar todo el código: Si copias todo el código antes de instalar dependencias, cualquier cambio en el código invalida el caché de npm install. Solución: copiar primero los archivos de dependencias, instalar, luego copiar el resto.
  3. No limpiar archivos temporales en la misma capa: Si usas apt-get install y luego rm -rf /var/lib/apt/lists/* en un RUN separado, los archivos temporales quedan en la capa anterior. Solución: combinar en un solo RUN: RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*.
  4. Usar imágenes base grandes innecesariamente: Prefiere variantes alpine o slim a menos que necesites herramientas específicas.
  5. No aprovechar build multi-etapa: Para aplicaciones compiladas, usa múltiples etapas para separar el entorno de build del de producción.

Checklist de dominio

  • He verificado que mi Dockerfile copia primero los archivos de dependencias antes de copiar el código fuente.
  • He combinado comandos RUN relacionados usando && para reducir capas.
  • He limpiado archivos temporales en la misma capa donde se generan.
  • He usado un archivo .dockerignore para excluir archivos innecesarios.
  • He considerado usar build multi-etapa si mi aplicación requiere compilación.
  • He elegido una imagen base pequeña (alpine, slim) adecuada para mi aplicación.
  • He ejecutado docker history en mi imagen para inspeccionar las capas y su tamaño.

Optimización de un Dockerfile existente

En este ejercicio, tomarás un Dockerfile no optimizado y lo refactorizarás siguiendo las buenas prácticas. El entregable es un archivo Dockerfile optimizado y un reporte breve (en texto) explicando los cambios realizados y su impacto.

Paso 1: Obtén el Dockerfile original

Crea un archivo Dockerfile con el siguiente contenido (simula una app Node.js):

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

Paso 2: Identifica problemas

Analiza el Dockerfile y lista al menos 3 problemas de optimización (por ejemplo, orden de capas, tamaño de imagen, etc.).

Paso 3: Refactoriza

Reescribe el Dockerfile aplicando: uso de .dockerignore, separación de copia de dependencias, build multi-etapa, y limpieza. Asume que la app tiene un package.json y package-lock.json.

Paso 4: Verifica

Construye ambas imágenes y compara tamaños con docker images. Ejecuta docker history en la imagen optimizada para ver las capas.

Rúbrica de evaluación (3 criterios)

  • Orden de capas (40%): Las capas que cambian frecuentemente están al final; las dependencias se instalan antes de copiar el código.
  • Uso de build multi-etapa (30%): Se utiliza una etapa de build y una etapa final que solo copia los artefactos necesarios.
  • Limpieza y tamaño (30%): Se eliminan archivos temporales en la misma capa y se usa una imagen base pequeña.
Pistas
  • Pista 1: Piensa en qué archivos cambian más a menudo (código fuente) y cuáles casi nunca (dependencias). Coloca los que cambian menos primero.
  • Pista 2: Para build multi-etapa, usa AS builder en la primera etapa y luego COPY --from=builder en la segunda.
  • Pista 3: No olvides crear un archivo .dockerignore con al menos node_modules y .git.