¿Qué son los Multi-stage Builds?
Los Multi-stage Builds son una característica de Docker que permite definir múltiples etapas dentro de un único Dockerfile. Cada etapa es esencialmente una imagen temporal que puede utilizar diferentes bases, instalar dependencias distintas y realizar tareas específicas. La magia radica en que solo la última etapa (o la que indiques explícitamente) se convierte en tu imagen final.
Esta técnica surge de una necesidad real: las imágenes de producción suelen incluir archivos de compilación, dependencias de desarrollo, herramientas de debugging y otros elementos que no son necesarios para ejecutar la aplicación, pero que incrementan significativamente el tamaño de la imagen.
El Problema: Imágenes Sobradas
Imagina que estás desarrollando una aplicación en Go. Para compilar tu programa necesitas:
- El SDK completo de Go (que puede superar los 800MB)
- Headers de sistema para compilación
- Bibliotecas de desarrollo
- Herramientas de build como gcc, make, etc.
Sin embargo, para ejecutar tu binario compilado de Go, solo necesitas el binario estático y potencialmente ninguna dependencia adicional si usas CGO_ENABLED=0. Tu imagen final termina pesando varios gigabytes cuando podría pesar apenas unos pocos megabytes.
Sintaxis Básica
La sintaxis es intuitiva. Cada etapa comienza con FROM y se le puede asignar un nombre con AS nombre. Para copiar artefactos entre etapas, se usa COPY --from=nombre o COPY --from=0 (usando el índice).
# Etapa 1: Construcción
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Etapa 2: Producción
FROM node:18-slim AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json .
CMD ["node", "dist/index.js"]Ejemplo Práctico: Aplicación React
Veamos un ejemplo completo con una aplicación React que requiere compilación:
# Etapa de build
FROM node:18-alpine AS build
WORKDIR /app
# Copiar solo archivos de dependencia primero (optimización de caché)
COPY package*.json ./
RUN npm ci
# Copiar código fuente
COPY . .
# Construir la aplicación
RUN npm run build
# Etapa de producción
FROM nginx:alpine AS production
# Copiar los archivos construidos
COPY --from=build /app/build /usr/share/nginx/html
# Copiar configuración de nginx
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Ejemplo Práctico: Aplicación Python
Para aplicaciones Python con dependencias complejas de compilación, los Multi-stage Builds son especialmente útiles:
# Etapa 1: Construir dependencias
FROM python:3.11-slim AS builder
# Instalar dependencias del sistema para compilar paquetes
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Instalar dependencias de Python
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Etapa 2: Producción
FROM python:3.11-slim AS production
# Crear usuario no root por seguridad
RUN groupadd --gid 1000 appgroup && \
useradd --uid 1000 --gid appgroup --shell /bin/bash appuser
WORKDIR /app
# Copiar solo lo necesario
COPY --from=builder /root/.local /home/appuser/.local
COPY --from=builder /app ./app
COPY app/ ./app
# Cambiar propietario
RUN chown -R appuser:appgroup /app
USER appuser
ENV PATH=/home/appuser/.local/bin:$PATH
CMD ["python", "app/main.py"]Copiar Artefactos Específicos
No siempre necesitas copiar todo desde una etapa. Docker permite copiar archivos específicos:
# Copiar directorio completo
COPY --from=builder /app/dist ./dist
# Copiar archivo individual
COPY --from=builder /app/package.json ./package.json
# Copiar desde múltiples etapas
COPY --from=frontend /app/build ./public
COPY --from=api /app/dist ./apiNombrar vs Índices
Tienes dos formas de referenciar etapas:
| Método | Sintaxis | Ejemplo | Uso recomendado |
|---|---|---|---|
| Índice | --from=0, --from=1 | --from=0 | Etapas simples |
| Nombre | --from=nombre | --from=builder | Etapas complejas |
Selección de Etapa Final
Normalmente, la última etapa es la que se convierte en imagen final. Sin embargo, puedes especificar una etapa diferente:
# Crear múltiples imágenes para diferentes propósitos
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Imagen de producción (la que se construye por defecto)
FROM alpine:3.18 AS production
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
# Imagen de debugging (si la necesitas)
FROM docker:dind AS debug
COPY --from=builder /app/myapp /myapp
# Construir etapa específica
# docker build --target production -t myapp:prod .
# docker build --target debug -t myapp:debug .Comparación de Tamaños
Veamos el impacto real con un ejemplo de aplicación Node.js:
| Método | Tamaño aproximado | Reducción |
|---|---|---|
| Sin Multi-stage (node:18 completo) | ~1.1 GB | - |
| Con Multi-stage (node:18-slim) | ~180 MB | 83% |
| Multi-stage optimizado | ~25 MB | 97% |
- Despliegues más rápidos
- Menos consumo de almacenamiento
- Transferencias más veloces
- Menor superficie de ataque
Patrón Common Base
Para proyectos con múltiples servicios, puedes usar un patrón de base común:
# Base común
FROM node:18-alpine AS base
WORKDIR /app
# Dependencias comunes
FROM base AS deps
COPY package*.json ./
RUN npm ci
# Servicio 1
FROM deps AS api
COPY src/api ./src
RUN npm run build:api
# Servicio 2
FROM deps AS web
COPY src/web ./src
RUN npm run build:web
# Imágenes finales
FROM node:18-alpine AS api-production
COPY --from=api /app/dist /app/dist
CMD ["node", "dist/api/index.js"]
FROM nginx:alpine AS web-production
COPY --from=web /app/dist /usr/share/nginx/htmlBuenas Prácticas
- Minimiza las capas: Combina comandos RUN cuando sea posible usando && y limpia cachés en la misma instrucción.
- Ordena las instrucciones: Pon lo que cambia menos primero para maximizar el uso de caché.
- Usa .dockerignore: Excluye node_modules, archivos de desarrollo y otros innecesarios.
- Prefiere imágenes oficiales minimalistas: alpine, slim, distroless.
- No installes herramientas de desarrollo en producción: git, curl, compiladores solo en etapas de build.
- Verifica tu imagen: Usa docker history para inspeccionar qué contiene cada capa.
# Instalar y limpiar en una sola capa
RUN apt-get update && \
apt-get install -y --no-install-recommends \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
# Verificar contenido de la imagen
docker history myapp:latestVer más: Ejemplo completo con múltiples tecnologíasUn caso avanzado: una aplicación con frontend (React), backend (Node.js) y worker (Python):
# ============================================
# FRONTEND: React con Vite
# ============================================
FROM node:18-alpine AS frontend-builder
WORKDIR /frontend
COPY frontend/package*.json .
RUN npm ci
COPY frontend/ .
RUN npm run build
# ============================================
# BACKEND: Node.js API
# ============================================
FROM node:18-alpine AS backend-builder
WORKDIR /backend
COPY backend/package*.json .
RUN npm ci
COPY backend/ .
RUN npm run build
# ============================================
# WORKER: Python con Celery
# ============================================
FROM python:3.11-slim AS worker-builder
WORKDIR /worker
COPY worker/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY worker/ .
# ============================================
# IMÁGENES FINALES
# ============================================
FROM nginx:alpine AS frontend
COPY --from=frontend-builder /frontend/dist /usr/share/nginx/html
FROM node:18-alpine AS backend
WORKDIR /app
COPY --from=backend-builder /backend/dist ./dist
COPY --from=backend-builder /backend/node_modules ./node_modules
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]
FROM python:3.11-slim AS worker
WORKDIR /app
COPY --from=worker-builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=worker-builder /app /app
CMD ["python", "-m", "celery", "-A", "tasks", "worker"]Verificación y Debugging
Después de implementar Multi-stage Builds, verifica que tu imagen contenga solo lo necesario:
# Ver todas las capas
docker history myapp:latest
# Ver archivos en la imagen final
docker run --rm myapp:latest ls -la /
# Ver procesos en ejecución
docker run --rm myapp:latest ps aux
# Ver tamaño desglosado
docker image inspect myapp:latest --format='{{.Size}}'Las imágenes pequeñas son imágenes seguras. Cada archivo innecesario es un vector potencial de ataque.
Resumen de Beneficios
| Beneficio | Impacto |
|---|---|
| Tamaño reducido | 80-95% menor que imágenes tradicionales |
| Seguridad mejorada | Menos superficie de ataque |
| Despliegues más rápidos | Menos datos a transferir |
| Mejor caching | Builds incrementales más eficientes |
| Mantenimiento sencillo | Un solo Dockerfile por servicio |
¿Cuál es la principal ventaja de usar Multi-stage Builds en Docker?
- A) Permiten usar múltiples archivos Dockerfile
- B) Reducen el tamaño de la imagen final al separar el entorno de construcción del de producción
- C) Aceleran la ejecución de contenedores
- D) Mejoran la seguridad del host únicamente
¿Cómo se referencian los archivos de una etapa anterior llamada "builder" en un Multi-stage Build?
- A) COPY builder:/app/dist ./dist
- B) COPY --from=1 /app/dist ./dist
- C) COPY --from=builder /app/dist ./dist
- D) COPY builder:0 /app/dist ./dist