Aprende Docker → Lección 5
Una imagen grande se paga en cada descarga, en cada despliegue y cada vez que el servicio escala. Lo bueno es que reducirla no suele costar trabajo: casi todo el peso sobra, y se quita con dos técnicas que caben en una lección.
El problema: el compilador viaja con la aplicación
Para construir una aplicación hacen falta herramientas que para ejecutarla no sirven de nada: compiladores, cabeceras, gestores de paquetes. Si todo eso se instala en la imagen final, se despliega con ella para siempre.
Este Dockerfile compila un programa en C y deja el compilador dentro:
FROM alpine:3.22 RUN apk add --no-cache gcc musl-dev WORKDIR /src COPY saluda.c . RUN gcc -static -O2 -o saluda saluda.c CMD ["/src/saluda"]
La solución: construir en varias etapas
Una construcción en varias etapas usa varios FROM en el mismo archivo. La primera etapa compila; la última parte de cero y copia solo el resultado. Todo lo demás se descarta.
FROM alpine:3.22 AS compilacion RUN apk add --no-cache gcc musl-dev WORKDIR /src COPY saluda.c . RUN gcc -static -O2 -o saluda saluda.c FROM scratch COPY --from=compilacion /src/saluda /saluda CMD ["/saluda"]
La diferencia medida sobre el mismo programa:
docker imagesmi-demo:multi 112kB mi-demo:gordo 253MB
las dos hacen exactamente lo mismohola desde una imagen mínima hola desde una imagen mínima
De 253 MB a 112 kB: unas dos mil veces menos, con el mismo comportamiento. La clave es COPY --from=compilacion, que trae un archivo de una etapa anterior sin arrastrar nada más de ella.
FROM scratch. Es la imagen vacía: ni sistema de archivos, ni intérprete de órdenes, ni nada. Solo funciona con binarios estáticos, que no dependen de bibliotecas del sistema, de ahí el -static al compilar. Para lenguajes como Go o Rust es habitual. Si tu programa necesita bibliotecas dinámicas, certificados o zona horaria, la última etapa debe ser alpine o distroless, no scratch.La segunda técnica: no enviar lo que no hace falta
Al construir, Docker empaqueta la carpeta entera y se la manda al demonio. Eso incluye el historial de git, las dependencias ya instaladas y cualquier archivo grande que ande por ahí, aunque el Dockerfile no los copie.
Un .dockerignore funciona como un .gitignore:
node_modules .git *.log .env target dist
Medido sobre una carpeta con 20 MB de dependencias y 5 MB de historial:
sin .dockerignoretransferring context: 25.01MB
con .dockerignoretransferring context: 118B
De 25 megabytes a 118 bytes. Eso es tiempo en cada construcción, y además evita el accidente de que un COPY . . meta en la imagen un archivo de credenciales que estaba en la carpeta.
RUN, encadenadas con &&, o resolverse con varias etapas como arriba.Por dónde empezar a recortar
| Medida | Cuánto suele ahorrar |
|---|---|
Imagen base -alpine o -slim |
Cientos de megabytes |
| Construcción en varias etapas | Casi todo lo que no es la aplicación |
.dockerignore |
Tiempo de construcción, y evita filtrar archivos |
Instalar y limpiar en el mismo RUN |
Lo que ocupe la caché del gestor de paquetes |
--no-cache en apk, --no-install-recommends en apt |
Decenas de megabytes |
Ver qué pesa con docker history |
Te dice dónde mirar |