18 puntos por xguru 2024-11-17 | 6 comentarios | Compartir por WhatsApp
  • 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.23 es 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_modules termina 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

  1. Compilar la aplicación en Dockerfile.build:
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. Copiar el artefacto compilado al host:
docker cp $(docker create build:v1):/app/.output .  
  1. 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.

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

 
savvykang 2024-11-18

En el caso de Java, jlink se introdujo a partir de la versión 9, pero su usabilidad no es buena porque hay que encontrar y especificar los módulos dependientes con jdeps, 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.

 
brainer 2024-11-17

Sí lo uso así, pero creo que la desventaja es que el tiempo de compilación tarda mucho.

 
kandk 2024-11-18

El tiempo de compilación no debería ser diferente. ¡Si hay diferencia, es porque está mal configurado!

 
brainer 2024-11-18

Ah, ya veo.

 
qurare 2024-11-18

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.

 
brainer 2024-11-18

¡Tendré que aprender un poco más sobre Docker!