# Optimiza tu Dockerfile: De 1GB a 22MB con Multi-Stage Builds. ¡Te explico cómo!

Canonical URL: https://luisalejandro.org/blog/posts/optimiza-tu-dockerfile-de-1gb-a-22mb-con-multi-stage-builds-te-explico-como

Published: 2025-07-14

Categories: DevOps

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.

```dockerfile
# 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.

```dockerfile
# --- 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](https://github.com/LuisAlejandro).