
Aprende a optimizar tu Dockerfile para reducir el tamaño de tu imagen Docker en un 98%. Descubre cómo las multi-stage builds mejoran la seguridad. ¡Entra ya!
Cuando empezamos a usar Docker, es muy común caer en la trampa de crear imágenes gigantescas y poco seguras. Metemos todo en un solo Dockerfile
: el código fuente, las dependencias de desarrollo, las herramientas de compilación... todo. Yo mismo lo hice durante mucho tiempo. El resultado es una imagen pesada, lenta de desplegar y con una superficie de ataque enorme. Hoy vamos a analizar por qué ese método de un solo paso es un problema y cómo las construcciones multi-etapa (multi-stage builds) son la solución definitiva para optimizar nuestras imágenes.
1. El método tradicional: La construcción de un solo paso
El enfoque más intuitivo es crear un Dockerfile
que hace todo en secuencia. Instala las dependencias, compila el código (si es necesario) y luego define cómo ejecutar la aplicación.
Veamos un ejemplo típico con una aplicación de Node.js. Queremos compilar nuestro frontend hecho con React.
❌ Incorrecto: Un Dockerfile
de una sola etapa.
# Base image con Node.js y todas las herramientas
FROM node:18
# Creamos el directorio de trabajo
WORKDIR /app
# Copiamos el package.json y package-lock.json
COPY package*.json ./
# Instalamos TODAS las dependencias (incluyendo las de desarrollo)
RUN npm install
# Copiamos todo el código fuente de la aplicación
COPY . .
# Hacemos el build de producción
RUN npm run build
# Exponemos el puerto y corremos la app (en este caso, un servidor para los estáticos)
CMD ["npx", "serve", "-s", "build", "-l", "3000"]
¿El problema aquí? Es gigantesco. La imagen final contiene:
- La versión completa de la imagen
node:18
. - Todo el directorio
node_modules
, que incluyedevDependencies
comowebpack
,babel
,jest
, etc. - Todo el código fuente original (
src/
,public/
, etc.), no solo los artefactos compilados.
Si construyes esta imagen, fácilmente podría pesar más de 1GB. Esto es ineficiente para el almacenamiento, lento para los pulls
y pushes
al registro, y un riesgo de seguridad porque estás desplegando herramientas de desarrollo en producción.
2. La solución: ¡Construcciones Multi-Etapa (Multi-stage Builds)!
Aquí es donde la magia ocurre. Una construcción multi-etapa te permite usar múltiples sentencias FROM
en un solo Dockerfile
. Cada FROM
inicia una nueva etapa de construcción que puede tener su propia imagen base y comandos. Lo más importante es que puedes copiar artefactos de una etapa a otra, descartando todo lo que no necesitas.
La idea es simple:
- Etapa de
build
: Usa una imagen con todas las herramientas necesarias para compilar tu código (comonode
,maven
,go
, etc.). Aquí instalas dependencias, corres tests y generas los artefactos de producción. - Etapa de
producción
: Empieza con una imagen base súper ligera (comonginx
,alpine
odistroless
). Aquí solo copias los artefactos generados en la etapa anterior. ¡Nada más!
3. Implementando una construcción Multi-Etapa
Vamos a reescribir nuestro Dockerfile
de Node.js usando este enfoque.
✅ Correcto: Un Dockerfile
multi-etapa.
# --- Etapa 1: Build Stage ---
# Usamos la imagen completa de Node para tener todas las herramientas de build
FROM node:18 AS builder
# Establecemos el directorio de trabajo
WORKDIR /app
# Copiamos los archivos de dependencias
COPY package*.json ./
# Instalamos las dependencias para compilar
RUN npm install
# Copiamos el resto del código fuente
COPY . .
# Generamos los artefactos de producción
RUN npm run build
# --- Etapa 2: Production Stage ---
# Empezamos desde una imagen súper ligera para servir contenido estático
FROM nginx:1.21-alpine
# Copiamos los artefactos de la etapa 'builder'
# Fíjate en el flag --from=builder. ¡Esa es la clave!
COPY --from=builder /app/build /usr/share/nginx/html
# Exponemos el puerto 80 por defecto de Nginx
EXPOSE 80
# Nginx se inicia automáticamente, así que no necesitamos un CMD
¡Mira la diferencia!
- Etapa
builder
: Esta primera etapa hace el trabajo sucio. Usa la imagennode:18
, instala todas lasdevDependencies
y ejecutanpm run build
. Al final de esta etapa, tenemos una carpeta/app/build
con nuestro JavaScript y CSS optimizados. - Etapa de Producción: Esta segunda etapa empieza desde cero con una imagen
nginx:1.21-alpine
, que es extremadamente pequeña. UsandoCOPY --from=builder ...
, copiamos únicamente la carpeta/app/build
desde la etapa anterior al directorio correcto de Nginx.
Todo lo demás de la primera etapa (node_modules
, el código fuente, los scripts
de build
) se descarta por completo.
4. Comparativa de resultados: Tamaño y Seguridad
Vamos a los números, que es lo que importa.
-
Imagen de una sola etapa:
- Tamaño: ~1.2 GB
- Contenido: Node.js,
npm
,node_modules
completos, código fuente, herramientas debuild
, etc. - Seguridad: Pobre. Si un atacante compromete el contenedor, tiene acceso a todo el entorno de desarrollo para explorar vulnerabilidades.
-
Imagen multi-etapa:
- Tamaño: ~22 MB
- Contenido: Nginx y los archivos estáticos de producción. Nada más.
- Seguridad: Excelente. No hay
npm
, no hayNode.js
, no hay código fuente nidevDependencies
. La superficie de ataque es mínima.
La reducción de tamaño no es del 10% o 20%, ¡es de más del 98%! Esto se traduce directamente en despliegues más rápidos, menores costos de almacenamiento y un ciclo de CI/CD mucho más ágil.
5. ¿Cuándo usar cada método?
Sinceramente, una vez que aprendes las construcciones multi-etapa, es difícil justificar el método de un solo paso para aplicaciones que requieren un build
.
- Usa Multi-Stage Builds siempre que tengas un paso de compilación: Esto aplica para JavaScript/TypeScript, Go, Rust, Java, C#, etc. Es el estándar de oro para crear imágenes de producción limpias.
- Usa Single-Step Builds solo para casos muy simples: Por ejemplo, si tu aplicación es solo un script de Python o Node.js que no necesita compilación y cuyas dependencias de producción son las mismas que las de desarrollo. Incluso en ese caso, podrías beneficiarte de una etapa para instalar dependencias de forma más limpia.
Así que ya lo sabes. Deja de crear imágenes de Docker obesas. Adopta las construcciones multi-etapa para separar tu entorno de build
del de runtime
. Tu infraestructura, tu equipo y tu factura de la nube te lo agradecerán.
Otros proyectos en mi perfil de GitHub.