Aquí hay algunos pensamientos sobre la optimización de imágenes de contenedores usando multi-stage builds e imágenes distroless.
Normalmente, al construir imágenes de contenedores para ejecutar aplicaciones, la imagen en sí suele incluir un sistema operativo completo, gestor de paquetes y utilidades del sistema, haciendo que el tamaño de la imagen sea de cientos de MB a GBs.
Este tamaño enorme para ejecutar un simple binario tiene un impacto en la velocidad de ejecución, costos de almacenamiento y postura de seguridad.
Los multi-stage builds y las imágenes distroless optimizan el tamaño de la imagen del contenedor mientras mantienen la funcionalidad a través de la selección de imagen base y la eliminación de dependencias durante ejecución innecesarias.

Build Tradicional de Una Sola Etapa
Por ejemplo, la mayoría de la gente empieza al contenedorizar una aplicación Java asi:
FROM openjdk:11-jdk
WORKDIR /app
COPY . /app
RUN javac Main.java
CMD ["java", "Main"]
Esto funciona bien. La aplicación se ejecuta. Pero la imagen final incluye todo el compilador JDK, herramientas de depuración y librerías. Todo lo cual es necesario para desarrollo, pero innecesario en ejecución.
La imagen final podría terminar con un tamaño de cientos de MB, para un archivo de clase Java compilado que podría ser de 5KB.
Multi-Stage Builds
Con multi-stage builds, puedes separar las dependencias de build-time de los requisitos en tiempo de ejecución. Compilas en una etapa usando todas las herramientas pesadas, y luego copias solo el artefacto final a una imagen de ejecución mínima.
Por ejemplo, para una aplicación Java contenedorizada, la etapa de build usa las herramientas y librerías completas del JDK, mientras que la imagen final en la etapa 2 usa solo el JRE-slim para ejecutar el código compilado. Todas las herramientas de desarrollo se quedan atrás. Esto reduce el tamaño de la imagen alrededor del 40% mientras mantiene la misma funcionalidad que los builds de una etapa.
# Etapa 1: Build
FROM openjdk:11-jdk AS build
WORKDIR /app
COPY . /app
RUN javac Main.java
# Etapa 2: Runtime
FROM openjdk:11-jre-slim
WORKDIR /app
COPY --from=build /app/Main.class /app/
CMD ["java", "Main"]
Imágenes Distroless
Las imágenes distroless llevan los esfuerzos de “minimización de imagen” un paso más allá. Por ejemplo, una imagen distroless podría ser de alrededor de 2MB.
Una imagen distroless elimina todo excepto lo que tu aplicación contenedorizada necesita para ejecutarse. Sin gestores de paquetes, sin shell, sin utilidades estándar de Unix. Solo la aplicación, sus dependencias directas de tiempo de ejecución y partes mínimas de una distribución Linux.
Por ejemplo, para una aplicación Java contenedorizada usando multi-stage builds con una imagen distroless, el tamaño de la imagen resultante sería mínimo.
# Etapa 1: Build
FROM openjdk:11-jdk AS build
WORKDIR /app
COPY . /app
RUN javac Main.java
# Etapa 2: Runtime
FROM gcr.io/distroless/java11-debian11
WORKDIR /app
COPY --from=build /app/Main.class /app/
CMD ["Main"]
Importante: La instrucción CMD en el Dockerfile/Containerfile debe usar la forma exec (["executable", "arg1", "arg2]) en lugar de la forma shell (executable arg1). Sin un shell en la imagen, la forma shell no funcionará.
Imágenes Distroless Disponibles
Google y Chainguard ofrecen imágenes distroless (base y específicas de lenguaje), que incluyen solo certificados CA y datos de zona horaria en sus imágenes base (lo esencial para la mayoría de las aplicaciones en red).
Google Distroless Images
- URL:
gcr.io/distroless. - Google usa imágenes basadas en Debian construidas con Bazel.
- Ofrece las imágenes distroless más populares.
Chainguard Images
- URL:
images.chainguard.dev/directory. - Chainguard usa imágenes basadas en Wolfi construidas con apko/melange.
- Se enfoca en parcheo de seguridad CVE más agresivo y rápido.
Qué No Está Disponible
Aplicaciones complejas como bases de datos no están disponibles como imágenes distroless. Por ejemplo, requieren herramientas de gestión de configuración, herramientas de backup y restauración, scripts, etc. Demasiado para una imagen mínima.
Algunas cosas no disponibles en forma distroless son:
- Bases de datos: PostgreSQL, MySQL, MongoDB necesitan herramientas de backup, CLIs de administración y utilidades de configuración.
- Servidores Web: Nginx, Apache requieren gestión de configuración y capacidades de recarga.
- Otros: Redis, Ruby, PHP (no hay imágenes oficiales disponibles).
El Desafío de la Depuración
El principal trade-off con las imágenes distroless es la depuración. Sin shell significa que no puedes hacer docker exec en un contenedor en ejecución para investigar problemas. Por diseño, no quieres esas herramientas en producción, pero requiere diferentes enfoques de depuración.
Variantes de Imagen para Debug
Google proporciona variantes de debug de sus imágenes distroless con busybox incluido para probar y depurar donde necesitas acceso shell.
FROM gcr.io/distroless/java11-debian11:debug
Una vez que la depuración esté terminada, elimínalas. El punto completo de distroless es minimizar la superficie de ataque.
Técnica de Compartir Namespaces
Puedes ejecutar un segundo contenedor busybox que comparta namespaces (PID y network) con tu contenedor distroless solo para propósitos de debug.
Esto permite que herramientas como ps y ss/netstat muestren los mismos procesos y conexiones que el contenedor objetivo.
# Iniciar contenedor distroless
docker run -d --name myapp gcr.io/distroless/static-debian12 /app
# Adjuntar un contenedor debugger
docker run --rm -it \
--name debugger \
--pid container:myapp \
--network container:myapp \
busybox sh
Este enfoque es ligero y no requiere reiniciar tu contenedor de aplicación.
Kubernetes Ephemeral Containers
Si estás ejecutando en Kubernetes, los ephemeral containers proporcionan depuración integrada:
kubectl debug -it <pod_name> \
--image=alpine \
--target=<container_name>
Esto crea un contenedor de depuración temporal que comparte namespaces con el pod objetivo. Similar al compartir namespaces regular pero integrado en Kubernetes.
Por Qué Importa el Tamaño de la Imagen
Cuando construyes una imagen de contenedor de la manera tradicional, todo lo usado durante el proceso de build termina en la imagen final.
Por ejemplo, si necesitas un compilador para construir la aplicación, estará incluido en la imagen. ¿Gestores de paquetes para instalar dependencias? También están ahí. Y todo estará en la imagen final en producción.
Esta hinchazón crea algunos problemas:
- Velocidad de Despliegue: Una imagen de 400MB toma mucho más tiempo en descargarse que una de 10MB. Al escalar horizontalmente o desplegar en múltiples hosts, los segundos extra se acumulan rápidamente.
- Costos de Infraestructura: Los registros de contenedores cobran basándose en almacenamiento y ancho de banda. Una imagen de 400MB almacenada en 10 versiones cuesta más dinero.
- Superficie de Seguridad: Cada binario, librería y paquete es una vulnerabilidad potencial. Los CVEs pueden impactar componentes que nunca usas pero que existen en la imagen. Imágenes más pequeñas significan menos componentes que parchear.
- Eficiencia de recursos: Imágenes más pequeñas significan inicios más rápidos, menos presión de memoria y I/O de disco más eficiente.
Con un tamaño de imagen pequeño, no solo estás ahorrando costos de espacio de almacenamiento, sino también mejorando tu postura de seguridad y haciendo el despliegue de aplicaciones más rápido.

¿Distroless, Alpine o Imágenes Estándar?
Alpine es una distribución Linux ligera. Su pequeño tamaño puede rivalizar con distroless y podría ser una mejor opción que distroless al construir una imagen de contenedor mínima.
La elección entre distroless, Alpine e imágenes mínimas estándar no se trata de que una sea “mejor”, como todo en la vida, depende de tu caso de uso.
Información de comparación:
| Aspecto | Distroless | Alpine | Standard Minimal (debian-slim) |
|---|---|---|---|
| Tamaño de Imagen | Más pequeño (static: ~2MB, Java: ~40-120MB) | Muy pequeño (base: ~5MB, Java: ~80-150MB) | Pequeño (base: ~25MB, Java: ~200-250MB) |
| Seguridad | Superficie de ataque mínima, sin shell | Superficie pequeña, incluye shell/gestor de paquetes | Superficie mayor, herramientas completas de userland |
| Depuración | Difícil (sin shell, sin utilidades) | Fácil (shell, herramientas Unix estándar) | Fácil (conjunto completo de herramientas disponible) |
| Recuento de CVE | Más bajo (paquetes mínimos) | Bajo (conjunto pequeño de paquetes) | Más alto (más paquetes instalados) |
| Compatibilidad | glibc estándar | musl libc (puede causar problemas de compatibilidad) | glibc estándar, compatibilidad más amplia |
| Caso de Uso | Producción, seguridad primero | Desarrollo, depuración, uso general | Aplicaciones legacy, compatibilidad máxima |
Cuándo Usar Distroless
- Entornos de Producción: Donde la seguridad y la eficiencia es más importante que la conveniencia de depuración. Usualmente tienes buen logging / observabilidad.
- Ejecutar Aplicaciones Statically-Linked: Al ejecutar programas Go y Rust sin dependencias. No se necesitan sistemas operativos o sistemas de empaquetado.
- La Seguridad es Máxima Prioridad: Donde la seguridad es lo principal y reducir la superficie de ataque y el aislamiento es muy importante.
Cuándo Usar Alpine
- Entornos de Desarrollo: Donde necesitas iteración rápida y la capacidad de instalar herramientas sobre la marcha a través de su gestor de paquetes.
- Se Necesita Depuración: Cuando necesitas solucionar problemas haciendo exec en contenedores. Alpine proporciona un shell y utilidades comunes sin hinchazón excesiva.
- Se Requieren Herramientas en Ejecución: Aplicaciones que necesitan
curl,wget,ncu otras utilidades en tiempo de ejecución. - No te Importa la Compatibilidad: Cuando tu aplicación no depende de características específicas de glibc. La mayoría de las aplicaciones funcionan bien con musl libc. Aplicaciones C/C++ compiladas contra glibc pueden tener problemas.
Cuándo Usar Imágenes Mínimas Estándar
Usa imágenes mínimas estándar como debian-slim o ubuntu-minimal cuando:
- Compatibilidad Máxima: Aplicaciones legacy que esperan librerías del sistema específicas o comportamiento de glibc.
- Cargas de Trabajo Mixtas: Al ejecutar múltiples aplicaciones con diferentes requisitos en el mismo cluster. Estandarizar en Debian-slim puede simplificar las operaciones incluso si no es óptimo para cada servicio.

Mejores Prácticas
Algunas cosas que he aprendido trabajando con imágenes distroless:
- Siempre Usa Multi-Stage Builds: Incluso si no estás usando una imagen distroless. La separación entre entornos de build y ejecución es una buena práctica independientemente.
- Fija Tus Versiones de Imagen Base: Usa tags o digests SHA256 en lugar de
latestpara usar una versión específica de imagen. Esto asegura builds reproducibles y hace los rollbacks predecibles. - Escanea Tus Imágenes Regularmente: Escanea tus imágenes de contenedores regularmente en busca de vulnerabilidades.
Al Final
Los multi-stage builds y las imágenes distroless son una solución simple y realmente buena para no solo reducir el tamaño de imagen de las imágenes de contenedores, sino también proporcionar mejor seguridad y velocidad, lo cual se vuelve cada vez más importante a medida que escalas. La clave es entender los trade-offs y por qué elegirías un build normal, distroless, una imagen mínima o alpine.