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
- No usar .dockerignore: Copiar
node_moduleso archivos temporales al contexto de build aumenta el tiempo de build y el tamaño de la imagen. Solución: crear un archivo.dockerignoreconnode_modules,.git, etc. - 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. - No limpiar archivos temporales en la misma capa: Si usas
apt-get instally luegorm -rf /var/lib/apt/lists/*en unRUNseparado, los archivos temporales quedan en la capa anterior. Solución: combinar en un soloRUN:RUN apt-get update && apt-get install -y ... && rm -rf /var/lib/apt/lists/*. - Usar imágenes base grandes innecesariamente: Prefiere variantes
alpineoslima menos que necesites herramientas específicas. - 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
RUNrelacionados usando&¶ reducir capas. - He limpiado archivos temporales en la misma capa donde se generan.
- He usado un archivo
.dockerignorepara 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 historyen 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.
- 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 builderen la primera etapa y luegoCOPY --from=builderen la segunda. - Pista 3: No olvides crear un archivo
.dockerignorecon al menosnode_modulesy.git.