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, nombradabuild, 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*.jsony ejecutarnpm 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: 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.*.mdycoverage: 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 | Sí | Desarrollo y depuración |
node:18-slim |
~250 MB | Sí | 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 libcybusybox, 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
- 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:
--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.
- 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 historyno 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 onode_modulesenormes. - Instalar herramientas de compilación en la imagen final: usa multi-stage builds para dejarlas fuera del resultado.
- Múltiples
RUNcon instalaciones y limpiezas separadas: la limpieza en una capa posterior no reduce el tamaño. Combina en un soloRUN. - Usar imágenes base enormes innecesariamente: prueba variantes
-slim,-alpineo distroless. - Consejo: mide siempre antes y después de optimizar con
docker imagesydive, 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"]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
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.
Docker: De Principiante a Avanzado
Módulo 1: Introducción a Docker
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Registro y Monitoreo en Docker
Módulo 6: Docker en Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
