La optimización de imágenes en Docker consiste en hacer que las imágenes sean más rápidas de construir y más pequeñas de ejecutar.
Has aprendido a escribir tu propio Dockerfile. Has ejecutado el comando de construcción y tu aplicación funciona perfectamente. Estás orgulloso, pero pronto te das cuenta de dos problemas que aparecen en cuanto el proyecto crece:
- Velocidad: Cada vez que cambias una simple línea de código o arreglas una errata, el comando
docker buildtarda 5 minutos en volver a descargar e instalar absolutamente todas las librerías del proyecto. - Tamaño: Tu imagen final de una simple aplicación “Hola Mundo” termina ocupando 800 MB o incluso gigabytes de espacio.
¿Cómo es posible que algo tan simple ocupe tanto disco duro? ¿Y por qué el motor repite procesos que ya había hecho antes?
En este artículo vamos a mejorar nuestros Dockerfiles sin cambiar la aplicación. Vamos a aprender a aprovechar la caché para builds casi instantáneos y a usar Multi-stage Builds para crear imágenes mucho más pequeñas.
La caché de capas
Recuerda que una imagen son capas apiladas. Cuando ejecutas docker build, Docker recorre tu Dockerfile instrucción por instrucción.
La idea importante es esta: si una instrucción no ha cambiado y los archivos que usa tampoco Docker reutiliza la capa que ya tenía guardada en caché.
Si Docker detecta un cambio en una línea (por ejemplo, has modificado el código fuente), invalida la caché para esa línea y para todas las líneas que van después.
El error común: Copiar todo al principio
Mira este Dockerfile típico cuando empezamos:
# MAL USO DE LA CACHÉ ❌
FROM node:18
WORKDIR /app
COPY . . # Aquí copias tu código
RUN npm install # Aquí instalas dependencias (lento)
CMD ["node", "app.js"]
¿Cuál es el problema? El código fuente (.) cambia constantemente (cada vez que editas un archivo).
- Docker llega a
COPY . .y ve que el código ha cambiado ➡️ Rompe la caché. - Llega a
RUN npm install. Como la capa anterior cambió, esta se ve obligada a ejecutarse de nuevo.
Resultado: Aunque solo hayas cambiado un console.log y no hayas tocado las librerías, Docker vuelve a descargar todo internet (npm install) cada vez.
Debemos copiar primero lo que menos cambia (las definiciones de dependencias) y después lo que más cambia (el código).
# BUEN USO DE LA CACHÉ ✅
FROM node:18
WORKDIR /app
# 1. Copiamos SOLO los archivos de definición de paquetes
COPY package.json package-lock.json .
# 2. Instalamos. Esta capa se cacheará y no se repetirá a menos que cambiemos dependencias.
RUN npm ci
# 3. AHORA copiamos el código fuente
COPY . .
CMD ["node", "app.js"]
Con este cambio, si modificas tu código:
- Docker ve que
package.jsonno ha cambiado ➡️ Reutiliza caché (Instante). - Docker ve que
RUN npm cino necesita ejecutarse ➡️ Reutiliza caché (Instante). - Solo ejecuta de nuevo el
COPY . .final.
Tu build pasa de tardar 3 minutos a tardar 0.5 segundos.
Multi-stage Builds (Reduciendo el tamaño)
Ahora vamos a por el tamaño. Para compilar una aplicación (Java, Go, C#, o un frontend de React), necesitas muchas herramientas: compiladores (GCC), SDKs, gestores de paquetes (Maven, Node)…
Pero para ejecutar la aplicación, solo necesitas el binario o los archivos estáticos Si usas una imagen base con todas las herramientas (FROM golang o FROM node), tu imagen final pesará gigas porque lleva el compilador dentro, aunque ya no lo necesites.
La solución son Multi-stage Builds (Construcción en etapas).
Esta técnica nos permite usar una imagen “sucia y pesada” para compilar, y luego copiar solo el resultado a una imagen “limpia y ligera” para producción. Todo en un solo Dockerfile.
Ejemplo Práctico: Una App de React/Web
Imagina que tienes una web en React. Necesitas Node.js para compilarla (npm run build), pero para servirla solo necesitas un servidor web ligero (Nginx).
# --- ETAPA 1: BUILDER (El obrero) ---
# Le ponemos un alias "builder" para referirnos a ella luego
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Esto crea la carpeta /app/dist con los HTML/CSS/JS optimizados
RUN npm run build
# --- ETAPA 2: RUNNER (El producto final) ---
# Empezamos DE CERO con una imagen vacía y ligera
FROM nginx:alpine
# Copiamos SOLO los archivos compilados de la etapa anterior
# --from=builder nos permite "robar" archivos de la otra imagen
COPY --from=builder /app/dist /usr/share/nginx/html
# No hace falta instalar Node, ni NPM, ni tener el código fuente original.
# Solo tenemos Nginx y los archivos estáticos.
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Resultados
| Método | Imagen Base | Tamaño Final |
|---|---|---|
| Tradicional | node:18 | ~ 1.1 GB |
| Multi-stage | nginx:alpine | ~ 25 MB |
Hemos reducido el tamaño en un 97%.
- Menos espacio en disco.
- Descargas más rápidas en el servidor.
- Más seguridad: Al no tener compiladores ni herramientas de desarrollo en la imagen final, si un hacker entra, no tiene herramientas (
npm,gcc,git) para escalar privilegios.
