Una imagen Docker de 1,2 GB para ejecutar un pequeño servicio en Go puede parecer simplemente un desperdicio de almacenamiento, pero el problema aparece de verdad cuando Kubernetes necesita crear nuevos pods bajo presión. Cada nodo que no tenga la imagen en caché debe descargarla antes de arrancar el contenedor. Separar la compilación del entorno de ejecución mediante un multi-stage build puede reducir radicalmente ese peso, aunque el salto concreto de 1,2 GB a 8 MB depende de la aplicación y no debe interpretarse como una cifra universal.
Las claves de las imágenes Docker pequeñas en 30 segundos
- Los multi-stage builds permiten compilar una aplicación en una imagen completa y copiar únicamente el resultado necesario a producción.
- Docker recomienda esta técnica para reducir tamaño y superficie de ataque.
scratches una imagen completamente vacía y resulta adecuada para binarios estáticos.- Una imagen oficial
golang:1.22ocupa alrededor de 285 MB comprimida en algunas variantes Linux, antes incluso de añadir código y capas propias. - Menos bytes pueden traducirse en despliegues y escalados más rápidos, especialmente cuando el nodo todavía no tiene las capas en caché.
El ejemplo es sencillo. Una aplicación en Go puede construirse perfectamente con un Dockerfile como este:
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]
Funciona.
El problema es que la imagen utilizada para compilar termina siendo también la imagen utilizada para ejecutar el servicio.
Eso significa transportar a producción herramientas que pueden haber sido necesarias durante el docker build, pero que ya no hacen nada cuando el binario está funcionando.
La documentación oficial de Docker describe precisamente este problema: en una construcción tradicional todas las instrucciones se ejecutan dentro del mismo entorno y las capas necesarias para descargar dependencias, compilar y empaquetar pueden terminar formando parte de la imagen final. Docker recomienda los multi-stage builds para separar ambos mundos.
El compilador no tiene por qué viajar a producción
La diferencia fundamental consiste en tratar la compilación y la ejecución como dos fases independientes.
Un Dockerfile para una aplicación Go sencilla podría quedar así:
FROM golang:1.22 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server .
FROM scratch
COPY --from=build /app/server /server
CMD ["/server"]
La primera fase puede ser tan pesada como sea necesario.
Contiene Go, las bibliotecas necesarias, herramientas de compilación y cualquier otra dependencia utilizada para producir el binario.
Cuando esa fase termina solo se copia /app/server hacia la segunda imagen.
El compilador se queda atrás.
El gestor de paquetes se queda atrás.
Las herramientas utilizadas durante el build también se quedan atrás.
Docker utiliza en su propia documentación prácticamente este mismo patrón para Go: una primera etapa basada en golang, seguida de otra basada en scratch, a la que se copia exclusivamente el ejecutable compilado.
scratch tiene además una característica peculiar: no contiene prácticamente nada porque representa una imagen base vacía.
No incluye una distribución Linux convencional, shell, gestor de paquetes ni las herramientas habituales de espacio de usuario.
Para un binario estático que no necesite ninguna de esas dependencias puede resultar una base extraordinariamente pequeña. Docker recomienda precisamente scratch como opción para binarios completamente estáticos.
De ahí pueden salir reducciones enormes.
Pero conviene matizar uno de los ejemplos habituales en redes sociales: pasar de 1,2 GB a 8 MB es perfectamente posible en una aplicación concreta, pero no significa que golang:1.22 ocupe por sí sola 1,2 GB.
Docker Hub muestra para golang:1.22.12-bookworm alrededor de 284,79 MB comprimidos en linux/amd64, mientras la variante Alpine se sitúa alrededor de 69,91 MB. Las capas adicionales de una aplicación, dependencias, artefactos, cachés y otros contenidos pueden elevar considerablemente el resultado final.
La comparación correcta sería por tanto:
| Diseño | Qué llega a producción | Tamaño posible |
|---|---|---|
| Una sola etapa | Runtime + compilador + herramientas + aplicación | Cientos de MB o más |
| Multi-stage + imagen mínima | Runtime mínimo + aplicación | Decenas de MB |
Multi-stage + scratch | Principalmente el binario y archivos copiados explícitamente | Puede bajar a pocos MB |
Los valores exactos dependen del programa compilado, arquitectura, símbolos, bibliotecas y archivos adicionales.
El tamaño importa cuando Kubernetes necesita reaccionar deprisa
Una imagen grande no implica necesariamente un arranque lento en todos los despliegues.
Docker y los runtimes de contenedores trabajan con capas y cachés. Si un nodo ya dispone de las capas necesarias, no necesita descargarlas nuevamente.
La situación cambia cuando aparece un nodo nuevo.
Puede suceder durante un escalado automático, después de sustituir una máquina, al mover cargas entre zonas o cuando Kubernetes programa el pod sobre un host que todavía no tiene esa versión de la imagen.
Antes de iniciar el contenedor hay que obtener las capas necesarias desde el registro.
Por eso el tamaño de la imagen afecta al denominado cold start del contenedor, aunque no sea su único componente.
Una imagen de 1,2 GB y otra de 8 MB representan una diferencia de 150 veces en volumen bruto. La diferencia temporal real dependerá del ancho de banda, latencia del registro, capas almacenadas previamente, velocidad del disco y paralelismo disponible.
A modo puramente matemático, transferir 1,2 GB por un enlace efectivo de 100 Mbit/s necesitaría alrededor de 96 segundos si se ignoraran protocolos, compresión y cualquier otro cuello de botella. Descargar 8 MB bajo esas mismas condiciones teóricas requeriría menos de un segundo.
En producción las cifras serán diferentes, pero la relación explica por qué la optimización puede importar durante un pico de tráfico.
El Horizontal Pod Autoscaler de Kubernetes puede decidir que hacen falta más réplicas rápidamente. Si esas réplicas necesitan decenas de segundos adicionales simplemente para obtener la imagen, la capacidad nueva puede incorporarse cuando buena parte del pico ya ha pasado.
La misma diferencia afecta a un despliegue sobre decenas o cientos de nodos.
No es solo espacio ocupado en un registry.
Es tráfico.
Es tiempo de despliegue.
Y también es recuperación ante fallos.
Menos contenido también reduce superficie de ataque, pero scratch tiene costes
Existe otra ventaja importante: todo software que no se incluye en una imagen deja de formar parte de su superficie de ataque.
Si el contenedor final no necesita un compilador, no existe demasiada razón para instalarlo.
Lo mismo ocurre con curl, wget, un gestor de paquetes o una colección completa de utilidades del sistema.
Docker señala expresamente que separar el entorno de compilación del runtime mediante multi-stage builds puede reducir tanto el tamaño de la imagen como su superficie de ataque.
Sin embargo, convertir scratch en una recomendación universal sería otro error.
Una imagen vacía tampoco incluye elementos que algunas aplicaciones necesitan.
Puede ser necesario incorporar certificados de autoridades de certificación (CA) para conexiones HTTPS, datos de zonas horarias o determinados archivos del sistema. Una aplicación que utilice CGO puede depender además de bibliotecas compartidas y no funcionar simplemente copiando su ejecutable a scratch.
También desaparecen las herramientas de diagnóstico.
Dentro de un contenedor scratch no existe normalmente:
sh
bash
curl
ps
cat
ls
Intentar ejecutar:
docker exec -it mi-contenedor /bin/sh
no funcionará si /bin/sh sencillamente no existe.
Desde seguridad puede ser una ventaja. Desde operaciones obliga a utilizar otras técnicas de depuración, contenedores efímeros o imágenes específicamente preparadas para diagnóstico.
Por eso una imagen mínima no siempre debe ser una imagen vacía.
Según la aplicación puede tener más sentido utilizar una base reducida que incluya las dependencias necesarias, mientras se mantiene el multi-stage build para eliminar todo el toolchain de compilación.
También ayuda revisar .dockerignore. Docker recomienda excluir del contexto elementos como .git, artefactos locales o directorios de dependencias que no necesitan transferirse durante el build.
La pregunta útil al revisar un Dockerfile es bastante sencilla:
¿este archivo o paquete es necesario para construir la aplicación o para ejecutarla?
Si únicamente pertenece al primer grupo, normalmente no debería viajar en la imagen de producción.
Una imagen pequeña no convierte automáticamente una aplicación en rápida, segura o eficiente. Puede existir un servicio de 20 MB con un arranque de dos minutos y otro de varios cientos de megabytes que responda inmediatamente.
Pero eliminar cientos de megabytes que nunca se utilizan es una de esas optimizaciones que actúan en varios sitios a la vez: disminuye transferencia, almacenamiento, tiempo de distribución y cantidad de software presente dentro del contenedor.
Para muchos servicios compilados, el cambio empieza simplemente con un segundo FROM.
Preguntas frecuentes
¿Qué es un multi-stage build en Docker?
Es un Dockerfile que utiliza varias instrucciones FROM para separar diferentes fases de construcción. Permite compilar la aplicación en una etapa y copiar solo los archivos necesarios a la imagen final.
¿Puede una imagen Docker pasar realmente de 1,2 GB a 8 MB?
Sí, es posible en determinados programas, especialmente con binarios estáticos pequeños, pero no es una reducción garantizada. El tamaño final depende de la aplicación, sus dependencias y los archivos que necesite durante la ejecución.
¿Es recomendable utilizar FROM scratch en producción?
Puede serlo para ejecutables completamente estáticos que no necesiten componentes adicionales del sistema. Otras aplicaciones requieren certificados, bibliotecas, zonas horarias o herramientas que obligan a utilizar una imagen base más completa.
¿Una imagen Docker más pequeña hace que Kubernetes escale más rápido?
Puede hacerlo cuando los nodos necesitan descargar la imagen antes de crear los nuevos pods. Si las capas ya están almacenadas localmente, la diferencia puede ser mucho menor.