Reducir el tamaño de las imágenes de contenedor con Docker Multi-Stage Build
(labs.iximiuz.com)- Al construir imágenes de contenedor de Docker, si el Dockerfile no tiene una estructura Multi-Stage, es muy probable que se incluyan archivos innecesarios
- Esto provoca un aumento en el tamaño de la imagen y también en las vulnerabilidades de seguridad
- Se analizan las principales causas de los “archivos innecesarios” que pueden aparecer en una imagen de contenedor y se explica cómo resolverlo con Multi-Stage Build
Causas del aumento de tamaño de la imagen
- Las aplicaciones tienen dependencias de tiempo de compilación y de tiempo de ejecución.
- Las dependencias de build son más numerosas que las de runtime y tienen más vulnerabilidades de seguridad (CVEs).
- Si se usa la misma imagen para compilar y ejecutar, se incluyen dependencias de build innecesarias (compiladores, linters, etc.).
- Las imágenes de build y runtime deberían estar separadas, pero esto suele pasarse por alto.
Ejemplos de estructura incorrecta de Dockerfile
Ejemplo incorrecto para una aplicación Go
FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o binary
CMD ["/app/binary"]
- La imagen
golang:1.23es para compilar, pero si se usa tal cual en producción, también incluye todo el compilador de Go y sus dependencias. - Tamaño de la imagen: más de 800MB, con más de 800 vulnerabilidades de seguridad.
Ejemplo incorrecto para una aplicación Node.js
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "/app/.output/index.mjs"]
- La carpeta
node_modulestermina incluyendo dependencias de desarrollo que no se necesitan en runtime. - No se puede resolver simplemente cambiando a
npm ci --omit=dev, porque eso podría eliminar dependencias de desarrollo necesarias durante el proceso de build.
Cómo se creaban imágenes Lean antes de Multi-Stage Build
Patrón Builder
- Compilar la aplicación en
Dockerfile.build:
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
- Copiar el artefacto compilado al host:
docker cp $(docker create build:v1):/app/.output .
- Crear la imagen de runtime en
Dockerfile.run:
FROM node:lts-slim
WORKDIR /app
COPY .output .
CMD ["node", "/app/.output/index.mjs"]
• Problemas: hay que escribir varios Dockerfile, gestionar el orden de build y usar scripts adicionales.
Entendiendo Multi-Stage Build
- Multi-Stage Build es una función que implementa el patrón Builder dentro de Docker.
- Permite definir las etapas de build y runtime en un solo Dockerfile usando múltiples instrucciones
FROM. - Usa la instrucción
COPY --from=<stage>para traer archivos compilados desde una etapa anterior.
- Permite definir las etapas de build y runtime en un solo Dockerfile usando múltiples instrucciones
Ejemplo de Dockerfile Multi-Stage (Node.js)
# Build stage
FROM node:lts-slim AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Runtime stage
FROM node:lts-slim AS runtime
WORKDIR /app
COPY --from=build /app/.output .
ENV NODE_ENV=production
CMD ["node", "/app/.output/index.mjs"]
- Al copiar directamente los artefactos compilados con
COPY --from=build, es posible mover los archivos sin pasar por el host.
Ejemplos prácticos de Multi-Stage Build
Aplicación React
# Build stage
FROM node:lts-slim AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Runtime stage
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
ENTRYPOINT ["nginx", "-g", "daemon off;"]
- Después del build, una aplicación React se convierte en archivos estáticos que pueden servirse con Nginx.
Aplicación Go
# Build stage
FROM golang:1.23 AS build
WORKDIR /app
COPY . .
RUN go build -o binary
# Runtime stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app/binary /app/binary
ENTRYPOINT ["/app/binary"]
- Usar una imagen distroless permite ofrecer un entorno de runtime minimizado.
Aplicación Java
# Build stage
FROM eclipse-temurin:21-jdk-jammy AS build
WORKDIR /build
COPY . .
RUN ./mvnw package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-jammy
COPY --from=build /build/target/app.jar /app.jar
CMD ["java", "-jar", "/app.jar"]
- Para el build se usa JDK, y para runtime se usa un JRE más ligero.
Conclusión
- Multi-Stage Build separa los entornos de build y runtime para evitar que el tamaño de la imagen crezca por dependencias de desarrollo innecesarias
- Esto permite reducir el tamaño de la imagen, reforzar la seguridad y simplificar el proceso de build
- Multi-Stage Build es un método estándar para crear imágenes de contenedor eficientes, y también soporta funciones avanzadas (por ejemplo, condiciones de ramificación y pruebas unitarias durante el build)
6 comentarios
En el caso de Java,
jlinkse introdujo a partir de la versión 9, pero su usabilidad no es buena porque hay que encontrar y especificar los módulos dependientes conjdeps, entre otras cosas. Al ver que la gente no conoce esos métodos o busca un JRE, parece que a las herramientas de Java les falta difusión, y da la impresión de que hace falta mejorarlas para que con un solo comando se genere un JRE.Sí lo uso así, pero creo que la desventaja es que el tiempo de compilación tarda mucho.
El tiempo de compilación no debería ser diferente. ¡Si hay diferencia, es porque está mal configurado!
Ah, ya veo.
Dependiendo de la estrategia, incluso puedes cachear una etapa completa, así que en mi caso más bien terminó reduciendo el tiempo de compilación.
¡Tendré que aprender un poco más sobre Docker!