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 incluye devDependencies como webpack, 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 (como node, 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 (como nginx, alpine o distroless). 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!

  1. Etapa builder: Esta primera etapa hace el trabajo sucio. Usa la imagen node:18, instala todas las devDependencies y ejecuta npm run build. Al final de esta etapa, tenemos una carpeta /app/build con nuestro JavaScript y CSS optimizados.
  2. Etapa de Producción: Esta segunda etapa empieza desde cero con una imagen nginx:1.21-alpine, que es extremadamente pequeña. Usando COPY --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 de build, 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 hay Node.js, no hay código fuente ni devDependencies. 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.