18 points par xguru 2024-11-17 | 6 commentaires | Partager sur WhatsApp
  • Lors de la construction d’une image de conteneur Docker, si le Dockerfile n’utilise pas une structure Multi-Stage, il y a de fortes chances qu’il inclue des fichiers inutiles
  • Cela entraîne une augmentation de la taille de l’image ainsi qu’une hausse des vulnérabilités de sécurité
  • L’article analyse les principales causes de ces « fichiers inutiles » dans une image de conteneur et explique comment les éliminer avec Multi-Stage Build

Causes de l’augmentation de la taille des images

  • Les applications ont des dépendances de build et d’exécution.
  • Les dépendances de build sont plus nombreuses que celles du runtime et comportent davantage de vulnérabilités de sécurité (CVE).
  • Si la même image est utilisée pour le build et l’exécution, elle embarque des dépendances de build inutiles (compilateurs, linters, etc.).
  • Les images de build et de runtime devraient être séparées, mais ce point est souvent négligé.

Exemple de structure Dockerfile incorrecte

Mauvais exemple pour une application Go

FROM golang:1.23  
WORKDIR /app  
COPY . .  
RUN go build -o binary  
CMD ["/app/binary"]  
  • L’image golang:1.23 est destinée à la compilation, mais si elle est utilisée telle quelle en production, elle embarque aussi l’ensemble du compilateur Go et ses dépendances.
  • Taille de l’image : plus de 800 Mo, avec plus de 800 vulnérabilités de sécurité.

Mauvais exemple pour une application 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"]  
  • Le dossier node_modules finit par inclure aussi des dépendances de développement inutiles au runtime.
  • On ne peut pas simplement corriger cela avec npm ci --omit=dev, car cela peut supprimer des dépendances de développement nécessaires au processus de build.

Méthode de création d’images Lean avant Multi-Stage Build

Pattern Builder

  1. Construire l’application dans Dockerfile.build :
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. Copier les artefacts construits vers l’hôte :
docker cp $(docker create build:v1):/app/.output .  
  1. Créer l’image de runtime dans Dockerfile.run :
FROM node:lts-slim  
WORKDIR /app  
COPY .output .  
CMD ["node", "/app/.output/index.mjs"]  
•	Problèmes : il faut écrire plusieurs `Dockerfile`, gérer l’ordre de build et ajouter des scripts supplémentaires.  

Comprendre Multi-Stage Build

  • Multi-Stage Build est une fonctionnalité Docker qui implémente le pattern Builder à l’intérieur même de Docker.
    • Avec plusieurs instructions FROM, on peut définir dans un seul Dockerfile les étapes de build et de runtime.
    • La commande COPY --from=<stage> permet de récupérer les fichiers construits depuis une étape précédente.

Exemple 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"]  
  • En copiant directement les artefacts construits avec COPY --from=build, on peut déplacer les fichiers sans passer par l’hôte.

Exemples concrets de Multi-Stage Build

Application 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;"]  
  • Après le build, une application React devient un ensemble de fichiers statiques qui peut être servi par Nginx.

Application 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"]  
  • L’utilisation d’une image distroless fournit un environnement d’exécution minimal.

Application 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"]  
  • Le build utilise un JDK, tandis que le runtime repose sur un JRE plus léger.

Conclusion

  • Multi-Stage Build sépare les environnements de build et de runtime, ce qui évite l’augmentation de la taille des images due à des dépendances de développement inutiles
  • Cela permet de réduire la taille des images, de renforcer la sécurité et de simplifier le processus de build
  • Multi-Stage Build est une méthode standard pour créer des images de conteneur efficaces, et elle prend aussi en charge des fonctions avancées (par ex. conditions de branchement, tests unitaires pendant le build)

6 commentaires

 
savvykang 2024-11-18

Dans le cas de Java, jlink a bien été introduit à partir de la version 9, mais son utilisation n’est pas très pratique, notamment parce qu’il faut identifier et déclarer explicitement les modules dépendants avec jdeps. Quand on voit que beaucoup de gens ne connaissent pas ce type de méthode ou cherchent encore un JRE, on a l’impression que la promotion des outils Java a été insuffisante, et qu’il faudrait les améliorer pour qu’un simple commande permette de générer un JRE.

 
brainer 2024-11-17

Je l’utilise comme ça, mais l’inconvénient, c’est que le temps de build est long.

 
kandk 2024-11-18

Le temps de build ne devrait pas être différent. S’il y a une différence, c’est que la configuration est incorrecte !

 
brainer 2024-11-18

Ah, d'accord !

 
qurare 2024-11-18

Selon la stratégie, on peut même mettre en cache une étape entière, donc j’ai au contraire constaté que le temps de build se raccourcissait !

 
brainer 2024-11-18

Je vais devoir en apprendre un peu plus sur Docker !