Réduire la taille des images de conteneur avec Docker Multi-Stage Build
(labs.iximiuz.com)- Lors de la construction d’une image de conteneur Docker, si le
Dockerfilen’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.23est 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_modulesfinit 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
- Construire l’application dans
Dockerfile.build:
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
- Copier les artefacts construits vers l’hôte :
docker cp $(docker create build:v1):/app/.output .
- 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 seulDockerfileles é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.
- Avec plusieurs instructions
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
Dans le cas de Java,
jlinka 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 avecjdeps. 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.Je l’utilise comme ça, mais l’inconvénient, c’est que le temps de build est long.
Le temps de build ne devrait pas être différent. S’il y a une différence, c’est que la configuration est incorrecte !
Ah, d'accord !
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 !
Je vais devoir en apprendre un peu plus sur Docker !