Multi-stage Builds para Imágenes Eficientes
Bienvenido a esta lección sobre Multi-stage Builds, una técnica fundamental que todo desarrollador Docker debería dominar para crear imágenes más pequeñas, seguras y eficientes.
¿Qué son los Multi-stage Builds?
Los Multi-stage Builds son una característica de Docker que permite utilizar múltiples etapas (stages) en un mismo archivo Dockerfile. Cada etapa puede utilizar una imagen base diferente, y únicamente los archivos necesarios se copian de una etapa a otra, descartando todo lo demás.
El Problema: Imágenes Monolíticas
Imagina una aplicación Node.js típica. Sin Multi-stage Builds, tu Dockerfile probablemente se vería así:
FROM node:18
WORKDIR /app
# Copiamos todo el código fuente
COPY . .
# Instalamos TODAS las dependencias (incluyendo devDependencies)
RUN npm install
# Compilamos la aplicación
RUN npm run build
# Ejecutamos
CMD ["node", "dist/index.js"]
Este enfoque parece funcionar, pero tiene problemas serios:
- Tamaño excesivo: La imagen puede superar los 800MB-1GB
- Superficie de ataque expandida: Más paquetes = más vulnerabilidades potenciales
- Tiempo de despliegue lento: Imágenes grandes tardan más en Push/Pull
- Secrets expuestos: Tokens y claves pueden quedar en capas intermedias
La Solución: Multi-stage Builds en Acción
Veamos cómo transformar el Dockerfile anterior usando Multi-stage Builds:
# ============================================
# ETAPA 1: Construcción (Builder Stage)
# ============================================
FROM node:18 AS builder
WORKDIR /app
# Copiamos primero package.json para cachear dependencias
COPY package*.json ./
# Instalamos SOLO dependencias de producción
RUN npm ci --only=production
# Copiamos el código fuente
COPY . .
# Compilamos la aplicación
RUN npm run build
# ============================================
# ETAPA 2: Producción (Production Stage)
# ============================================
FROM node:18-slim AS production
# Creamos un usuario no-root por seguridad
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
WORKDIR /app
# Copiamos SOLO lo necesario del builder
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json
# Cambiamos al usuario no-root
USER nextjs
EXPOSE 3000
CMD ["node", "dist/index.js"]
- Etapa 1 (builder): Utiliza la imagen completa de Node.js con todas las herramientas de desarrollo
- Etapa 2 (production): Utiliza
node:18-slim, una imagen mínima de ~120MB - COPY --from=builder: Copia exclusivamente los archivos compilados y node_modules de producción
- USER nextjs: Cambia a un usuario no-root por razones de seguridad
Comparativa de Resultados
| Aspecto | Dockerfile Tradicional | Multi-stage Build |
|---|---|---|
| Tamaño de imagen | ~950 MB | ~180 MB |
| Dependencias de compilación | Incluidas | Descartadas |
| Usuario root | Sí (por defecto) | No (usuario personalizado) |
| Caché de capas | Parcial | Optimizada |
| Tiempo de Push/Pull | ~5-10 min | ~1-2 min |
node:18-slim o node:18-alpine para producción. La diferencia entre node:18 (970MB) y node:18-slim (180MB) es dramática en escenarios reales.Multi-stage Builds con Diferentes Lenguajes
Ejemplo: Aplicación Go
Go es famoso por poder compilar binarios estáticos autocontenidos. Los Multi-stage Builds permiten crear imágenes increíblemente pequeñas:
# Etapa 1: Compilación
FROM golang:1.21-alpine AS builder
WORKDIR /build
# Instalar herramientas necesarias
RUN apk add --no-cache git
# Copiar y descargar dependencias
COPY go.mod go.sum ./
RUN go mod download
# Copiar código fuente
COPY . .
# Compilar binario estático (sin CGO)
ENV CGO_ENABLED=0
RUN go build -ldflags='-w -s' -o main .
# Etapa 2: Imagen final mínima
FROM scratch
# Copiar证书 si necesitas HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copiar el binario compilado
COPY --from=builder /build/main /main
ENTRYPOINT ["/main"]
FROM scratch, la imagen final puede ser de apenas ~15-20 MB para una aplicación Go compilada. No tiene sistema operativo, solo el binario y lo mínimo indispensable.Ejemplo: Aplicación Python
# Etapa 1: Builder
FROM python:3.11-slim AS builder
WORKDIR /app
# Instalar pip-tools y crear entorno virtual
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Instalar solo dependencias de producción
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/opt/venv -r requirements.txt
# Etapa 2: Producción
FROM python:3.11-slim AS production
# Copiar el entorno virtual del builder
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
# Usuario no-root
RUN useradd --create-home appuser && \
chown -R appuser:appuser /app
USER appuser
CMD ["python", "app.py"]
Patrones Avanzados
Múltiples Etapas con Nombres
Puedes nombrar cada etapa y referenciarlas específicamente:
FROM node:18 AS frontend-builder
# ... build frontend ...
FROM maven:3.9-eclipse-temurin-17 AS backend-builder
# ... build backend ...
FROM nginx:alpine AS web-server
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
FROM openjdk:17-slim AS api-server
COPY --from=backend-builder /app/target/app.jar /app/
CMD ["java", "-jar", "/app/app.jar"]
Etapas Intermedias para Tests
Ver más: Ejecutar Tests en Pipeline de Build# Builder stage
FROM node:18 AS builder
WORKDIR /app
COPY . .
RUN npm ci
# Test stage
FROM builder AS tester
RUN npm test || exit 1
# Production stage (solo se ejecuta si tests pasan)
FROM node:18-slim AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
Este patrón es excelente para CI/CD: si los tests fallan, la construcción se detiene automáticamente antes de generar la imagen de producción.
Mejores Prácticas
- Ordena las capas por frecuencia de cambio: Copia primero archivos que cambian poco (package.json, requirements.txt)
- Usa --chown en Producción: Siempre asigna propiedad correcta a los archivos copiados
- Minimiza el número de etapas: Más etapas = más complejidad. Usa las necesarias.
- Limita el uso de --mount: Para caché de build (Maven, npm), usa --mount=type=cache
- Valida con dive: Herramienta para inspeccionar qué contiene realmente tu imagen final
Optimización con BuildKit Cache
# Habilitar BuildKit para caché de dependencias
# Set DOCKER_BUILDKIT=1
FROM node:18 AS builder
WORKDIR /app
# Montar caché de npm
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Montar caché de build
RUN --mount=type=cache,target=/app/node_modules \
npm run build
~/.docker/config.json:
{
"features": {
"buildkit": true
}
}
Esto habilita caché entre builds y acelera significativamente reconstrucciones incrementales.Depuración de Multi-stage Builds
Si necesitas inspeccionar una etapa específica durante el desarrollo:
# Construir solo hasta la etapa 'builder'
docker build --target builder -t my-app:debug .
# Abrir shell interactivo en esa etapa
docker run -it my-app:debug /bin/sh
--target, Docker construye todas las etapas anteriores necesarias pero se detiene en la etapa especificada. Esto es invaluable para depurar problemas de compilación sin generar la imagen completa.Resumen y Próximos Pasos
Los Multi-stage Builds son una de las características más poderosas de Docker moderno. No solo reducen el tamaño de las imágenes, sino que mejoran la seguridad, aceleran los despliegues y hacen más eficiente todo el pipeline de desarrollo.
En esta lección aprendiste:
- ✅ Qué son los Multi-stage Builds y por qué importan
- ✅ Cómo transformar un Dockerfile tradicional en uno eficiente
- ✅ Ejemplos prácticos con Node.js, Go y Python
- ✅ Patrones avanzados y mejores prácticas
- ✅ Técnicas de depuración para desarrollo
Recursos Adicionales
Ver más: Herramientas Recomendadas- dive: Analiza el contenido de tus imágenes Docker
- docker-slim: Automatiza la reducción de imágenes
- hadolint: Linter para Dockerfiles con reglas de mejores prácticas
- buildpacks: Construye imágenes sin Dockerfiles
¿Cuál es el beneficio principal de usar Multi-stage Builds en Docker?
- A) Aumentar la velocidad de construcción del código
- B) Reducir el tamaño de la imagen final manteniendo solo lo esencial
- C) Facilitar la escritura de tests automatizados
- D) Mejorar el rendimiento de la aplicación en producción
¿Qué instrucción de Dockerfile permite copiar archivos desde una etapa específica a otra?
- A) COPY --from=stage-name
- B) FROM --stage=stage-name
- C) SELECT --from stage-name
- D) EXTRACT --source=stage-name
COPY --from=stage-name copia artefactos específicos desde una etapa intermedia. También puedes usar números: COPY --from=0 para la primera etapa.