Una imagen Docker bien optimizada se construye más rápido, ocupa menos espacio, se descarga antes y expone una superficie de ataque menor. En esta lección aprenderemos las técnicas clave para conseguirlo: las construcciones multi-etapa (multi-stage builds), el aprovechamiento inteligente de la caché de capas, el fichero .dockerignore, el uso de imágenes mínimas como Alpine o distroless, la combinación de comandos RUN y el análisis del tamaño con docker history y dive. Estas prácticas marcan la diferencia entre una imagen profesional y una pesada e ineficiente.

Multi-Stage Builds

Las construcciones multi-etapa permiten usar una imagen para compilar la aplicación (con todas las herramientas de desarrollo) y otra mucho más ligera para ejecutarla (solo con lo imprescindible). El resultado final descarta todo lo necesario para compilar.

# Etapa 1: construcción
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Etapa 2: ejecución (imagen final ligera)
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  • FROM node:18 AS build: primera etapa, nombrada build, con todo el entorno de Node para compilar.
  • RUN npm run build: genera los artefactos finales (por ejemplo, en /app/dist).
  • FROM nginx:alpine: segunda etapa, una imagen mínima que solo sirve el contenido estático.
  • COPY --from=build /app/dist ...: copia únicamente los artefactos compilados desde la etapa anterior.

El resultado es una imagen final pequeña que no contiene ni Node, ni dependencias de desarrollo, ni código fuente.

Orden de Capas y Caché

Docker construye las imágenes por capas y cachea cada instrucción. Si una capa no cambia, Docker reutiliza la versión cacheada en lugar de reconstruirla. La clave está en ordenar las instrucciones de menos a más cambiantes.

FROM node:18-alpine
WORKDIR /app

# 1. Primero las dependencias (cambian poco)
COPY package*.json ./
RUN npm ci

# 2. Después el código fuente (cambia a menudo)
COPY . .

CMD ["node", "server.js"]
  • Al copiar primero package*.json y ejecutar npm ci, esa capa solo se reconstruye cuando cambian las dependencias.
  • Como el código fuente se copia después, cambiar una línea de código no invalida la costosa capa de instalación de dependencias.

Si hicieras COPY . . antes de instalar dependencias, cualquier cambio mínimo en el código forzaría reinstalar todo, ralentizando enormemente las builds.

Orden Consecuencia
Dependencias antes del código La caché de dependencias se reutiliza (rápido)
Código antes de las dependencias Cada cambio de código reinstala todo (lento)

El Fichero .dockerignore

Por defecto, COPY . . envía todo el directorio (el contexto de build) al demonio de Docker, incluyendo ficheros innecesarios o sensibles. El fichero .dockerignore evita copiar lo que no debe estar en la imagen.

node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
.env
*.md
dist
coverage
  • node_modules: se reinstalan dentro del contenedor; copiarlas es lento y puede causar incompatibilidades.
  • .git: el historial de versiones no debe acabar en la imagen.
  • .env: evita filtrar secretos en la imagen.
  • *.md y coverage: documentación e informes de pruebas que no aportan nada en producción.

Beneficios: builds más rápidas (menos contexto que transferir), imágenes más pequeñas y menor riesgo de filtrar información sensible.

Reducir el Tamaño: Alpine y Distroless

Imagen base Tamaño aproximado Incluye shell Caso de uso
node:18 ~1 GB Desarrollo y depuración
node:18-slim ~250 MB Equilibrio tamaño/compatibilidad
node:18-alpine ~180 MB Sí (busybox) Producción ligera
distroless/nodejs ~150 MB No Producción, máxima seguridad
  • Alpine se basa en musl libc y busybox, lo que la hace muy pequeña. Algunas dependencias compiladas pueden requerir ajustes, pero para la mayoría de casos es ideal.
  • Distroless contiene solo el runtime y la aplicación, sin shell ni gestor de paquetes, lo que reduce tamaño y superficie de ataque.

Combinar Comandos RUN

Cada instrucción RUN crea una capa. Encadenar comandos en un solo RUN y limpiar en la misma capa evita dejar residuos que aumentan el tamaño.

# MAL: tres capas y la caché de apt queda en la imagen
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# BIEN: una sola capa y limpieza en el mismo paso
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*
  • &&: encadena los comandos para que se ejecuten en una única capa.
  • --no-install-recommends: evita instalar paquetes recomendados que no necesitas.
  • rm -rf /var/lib/apt/lists/*: borra la caché de paquetes en la misma capa. Si se hiciera en una capa posterior, los ficheros ya estarían grabados en la capa anterior y no se reduciría el tamaño.

Análisis del Tamaño: docker history y dive

docker history

docker history mi_imagen:1.0
  • Muestra cada capa de la imagen, el comando que la creó y su tamaño. Permite identificar qué instrucción está engordando la imagen.

Para una salida más legible:

docker history --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" mi_imagen:1.0
  • --no-trunc: no recorta los comandos largos.
  • --format: muestra solo el tamaño y el comando de cada capa.

dive

dive es una herramienta interactiva que analiza imagen capa por capa y muestra qué ficheros añade cada una, así como el espacio desperdiciado.

dive mi_imagen:1.0
  • Indica un porcentaje de eficiencia de la imagen y resalta ficheros duplicados o eliminados en capas posteriores que siguen ocupando espacio.
  • Es excelente para detectar oportunidades de optimización que docker history no muestra con tanto detalle.

Errores Comunes y Consejos

  • Copiar el código antes de instalar dependencias: invalida la caché en cada cambio. Copia primero los manifiestos de dependencias.
  • No usar .dockerignore: ralentiza las builds y puede filtrar secretos o node_modules enormes.
  • Instalar herramientas de compilación en la imagen final: usa multi-stage builds para dejarlas fuera del resultado.
  • Múltiples RUN con instalaciones y limpiezas separadas: la limpieza en una capa posterior no reduce el tamaño. Combina en un solo RUN.
  • Usar imágenes base enormes innecesariamente: prueba variantes -slim, -alpine o distroless.
  • Consejo: mide siempre antes y después de optimizar con docker images y dive, para comprobar el impacto real de tus cambios.

Ejercicios

Ejercicio 1: Multi-stage build

Convierte un Dockerfile de una sola etapa que compila y ejecuta una aplicación Node en uno multi-etapa que solo incluya los artefactos finales. Compara el tamaño de ambas imágenes con docker images.

Ejercicio 2: Optimizar la caché

Reordena un Dockerfile para que las dependencias se instalen antes de copiar el código. Realiza dos builds (una con un cambio de código) y observa cómo la segunda reutiliza la caché de dependencias.

Ejercicio 3: Análisis con dive

Construye una imagen, analízala con dive y docker history, identifica la capa más pesada y propón una mejora.

Soluciones

Solución 1

# Multi-stage
FROM node:18 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]
docker build -t app:multi .
docker images | grep app

La imagen multi-etapa será notablemente más pequeña que la de una sola etapa, al no incluir dependencias de desarrollo ni el código fuente sin compilar.

Solución 2

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
docker build -t app:cache .
# Cambia una línea en un fichero de código fuente y reconstruye
docker build -t app:cache .

En la segunda build, Docker mostrará Using cache en la capa de npm ci, porque las dependencias no han cambiado; solo se reconstruyen las capas a partir de COPY . ..

Solución 3

docker build -t app:analisis .
docker history app:analisis
dive app:analisis

Normalmente la capa más pesada es la de instalación de dependencias o la imagen base. Mejoras posibles: cambiar a una base -alpine, eliminar dependencias de desarrollo con --omit=dev, o limpiar cachés de paquetes en la misma capa RUN.

Conclusión

En esta lección hemos aprendido a optimizar imágenes Docker mediante construcciones multi-etapa, el aprovechamiento de la caché de capas, el fichero .dockerignore, el uso de imágenes mínimas como Alpine y distroless, la combinación de comandos RUN y el análisis del tamaño con docker history y dive. Imágenes más pequeñas y rápidas se traducen en despliegues más ágiles, costes menores y mayor seguridad.

En la siguiente lección, Registro y Monitoreo en Docker, veremos cómo observar el comportamiento de los contenedores en ejecución: logs, logging drivers, métricas en tiempo real, healthchecks y una introducción a la monitorización con cAdvisor, Prometheus y Grafana.

© Copyright 2026. Todos los derechos reservados