- Implementación de builds multi-etapa para separar la compilación de la ejecución.
- Uso de imágenes Distroless y Scratch para eliminar el sistema operativo y reducir vulnerabilidades.
- Gestión eficiente de capas y dependencias para optimizar el almacenamiento y el despliegue.

A menudo nos encontramos con que nuestras imágenes de contenedor han acabado pesando un gigabyte o incluso dos, lo cual es una auténtica locura. No se trata solo de que el almacenamiento se dispare, sino de que estamos arrastrando paquetes del sistema operativo y librerías que no sirven para nada en el entorno de producción, convirtiendo nuestro despliegue en un lastre. Para evitar esto, es fundamental saber cómo trabajar con Docker de manera eficiente.
Esta acumulación de archivos no es solo un problema de rendimiento; es un riesgo real. Cuantas más librerías instalemos, mayor será la superficie de ataque para un hacker, lo que multiplica las vulnerabilidades y el riesgo de seguridad. Si queremos que nuestros despliegues vuelen y sean seguros, tenemos que aprender a limpiar la casa y dejar solo lo imprescindible.
El arte de los Multi-stage Builds
Los Dockerfiles multi-stage son una funcionalidad brutal que nos permite usar varias instrucciones FROM en un mismo archivo. Básicamente, podemos crear una etapa de construcción donde tengamos todas las herramientas pesadas (compiladores, gestores de paquetes, SDKs) y luego una etapa final mucho más ligera donde solo copiamos los artefactos ya compilados. Esta técnica es clave en cualquier guía avanzada para optimizar builds de Docker y acelerar el CI/CD.
Por ejemplo, en el ecosistema de Java, lo ideal es usar una imagen con el JDK para compilar la aplicación y, posteriormente, mover el archivo .jar a una imagen que solo contenga el JRE. En lenguajes como Go o Rust, la mejora es todavía más drástica, ya que una vez generado el binario estático, no necesitamos absolutamente nada más para que la aplicación funcione.
Para que nos quede más claro, veamos qué dependencias podemos eliminar según el lenguaje:
- Java: Pasamos del JDK en construcción al JRE en ejecución.
- Go/Rust: Eliminamos el compilador y Cargo, dejando solo el binario.
- Python: Quitamos Pip y las herramientas de desarrollo, manteniendo solo el runtime.
- TypeScript: Pasamos de Node.js con herramientas de build a un entorno de ejecución limpio.
La gran ventaja de este enfoque es que obtenemos imágenes significativamente más pequeñas, lo que repercute en despliegues más veloces y un consumo de recursos mucho más eficiente en la nube.
Entendiendo el concepto Distroless

Si el multi-stage es un gran paso, el enfoque distroless es llevar la optimización al extremo. Estas imágenes, impulsadas principalmente por Google, no incluyen una distribución de Linux completa. No hay gestores de paquetes, ni siquiera hay un intérprete de comandos (shell). Solo contienen lo estrictamente necesario para que el runtime del lenguaje funcione.
Esto es un sueño para la seguridad. Si un atacante lograra entrar en el contenedor, se encontraría con que no hay shell disponible para ejecutar comandos, eliminando así el vector de ataque más común. Además, al no tener paquetes innecesarios de Debian o Ubuntu, el número de CVEs detectados en los escaneos de seguridad cae en picado.
Existen diferentes sabores de imágenes según lo que necesitemos:
- Static: Muy básica, incluye certificados CA y tzdata.
- Base: Añade glibc, libssl y openssl, siendo la opción recomendada para la mayoría.
- Específicas: Versiones optimizadas para Java, Python, Node.js, .NET y Rust.
Eso sí, no todo es perfecto. La principal pega es que depurar en producción es más complicado porque no puedes hacer un «exec» y entrar a mirar archivos. Para solucionar esto, existen herramientas como Docker Debug o el uso de imágenes con BusyBox integradas.
Estrategias avanzadas de gestión de capas
Es fundamental entender que cada instrucción en un Dockerfile crea una capa. Si no tenemos cuidado, podemos terminar con una imagen hinchada por archivos temporales que, aunque los borremos en una línea posterior, siguen existiendo en las capas inferiores. Para evitar esto, debemos encadenar los comandos usando &&.
Otra buena práctica es organizar el archivo para aprovechar la caché. Debemos colocar las dependencias que menos cambian al principio y el código de la aplicación al final. De este modo, si solo cambiamos una línea de código, Docker no tiene que volver a instalar todas las librerías del sistema, acelerando la construcción hasta en un 70%.
Tampoco podemos olvidarnos del archivo .dockerignore. Si no lo usamos, podríamos estar copiando accidentalmente la carpeta node_modules o el historial de Git dentro de la imagen, lo que aumenta el peso innecesariamente y puede exponer secretos del código fuente.
Herramientas de análisis y optimización
A veces vamos a ciegas y no sabemos qué es lo que realmente está ocupando espacio. Para eso existe Dive, una herramienta open source espectacular que nos permite bucear en las capas de la imagen. Nos muestra exactamente qué archivos se añadieron o modificaron en cada paso y nos avisa sobre el espacio desperdiciado.
Además de Dive, existen opciones como Docker Slim, que analiza la imagen y elimina automáticamente aquello que no se utiliza durante la ejecución. Si trabajamos con aplicaciones estáticas extremas, podemos llegar a usar imágenes scratch, que son literalmente imágenes vacías (0 MB), donde solo ponemos nuestro binario compilado.
Para los que buscan un equilibrio, Alpine Linux sigue siendo la opción reina por su tamaño mínimo (unos 5 MB), aunque las distroless de Google suelen ser incluso más ligeras en casos específicos. Al final, se trata de minimizar la huella para ganar en velocidad de arranque y seguridad.
La clave para lograr contenedores profesionales reside en combinar la selección de imágenes base mínimas, el uso estratégico de builds multi-etapa y la eliminación agresiva de cualquier herramienta que no sea vital para el runtime, asegurando así un ecosistema de despliegue optimizado, la reducción de costes en infraestructura cloud y una superficie de ataque mínima frente a posibles intrusiones.

