Une image Java construite avec Maven pèse souvent un gigaoctet pour livrer un jar de quelques mégaoctets. Le build multi-étapes la ramène à sa taille utile.
Vous venez de conteneuriser une application Java. Le Dockerfile fonctionne, le conteneur démarre, les tests passent. Puis vous tapez docker images et vous découvrez que votre application de quelques mégaoctets voyage dans une image de 900 Mo.
Ce n'est ni une erreur de configuration ni un problème de Docker. C'est la conséquence directe d'une chose simple : vous avez livré votre atelier avec le produit fini.
Prenons le Dockerfile que presque tout le monde écrit au début :
FROM maven:3.9-eclipse-temurin-21
WORKDIR /app
COPY . .
RUN mvn package
CMD ["java", "-jar", "target/app.jar"]L'image de départ, maven:3.9-eclipse-temurin-21, contient le JDK complet, Maven, et les outils nécessaires à la compilation. À cela s'ajoute, pendant le mvn package, le dépôt local ~/.m2 rempli de toutes les dépendances téléchargées, plus le dossier target/ avec ses classes compilées et ses rapports de tests.
Or, pour exécuter l'application, il ne faut rien de tout ça. Il faut une JVM et un fichier .jar.
| Ce qui est dans l'image | Nécessaire pour compiler | Nécessaire pour exécuter |
|---|---|---|
| Compilateur Java (JDK) | Oui | Non |
| Maven | Oui | Non |
Dépôt local ~/.m2 | Oui | Non |
| Classes intermédiaires, rapports de tests | Oui | Non |
| Machine virtuelle Java (JRE) | — | Oui |
Le fichier .jar | — | Oui |
Deux lignes sur six servent en production. Le reste occupe de la place, allonge chaque docker pull, et surtout élargit la surface d'attaque : un compilateur dans un conteneur exposé sur Internet est un outil offert à qui saura s'en servir.
Un build multi-étapes découpe le Dockerfile en plusieurs images successives, dont une seule est conservée. Les précédentes servent d'échafaudage et sont jetées.
Concrètement :
# Étape 1 : on compile, avec tout l'outillage nécessaire
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# Les dépendances d'abord, séparément du code : tant que le pom.xml ne change
# pas, Docker réutilise cette couche et ne retélécharge rien.
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Étape 2 : on n'emporte que le résultat
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/app.jar ./app.jar
# Ne jamais tourner en root sans raison : si le processus est compromis,
# l'attaquant hérite de ses droits.
RUN addgroup -S app && adduser -S app -G app
USER app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]La ligne qui fait tout le travail est COPY --from=builder. Elle prend un fichier dans l'étape précédente et l'amène dans la nouvelle. Tout ce qui n'est pas explicitement copié disparaît.
Sur une application Spring Boot ordinaire, l'ordre de grandeur est le suivant :
21-jre-alpine : environ 180 à 200 MoLes chiffres exacts dépendent de vos dépendances, mais le rapport de quatre à cinq se vérifie presque toujours, parce qu'il vient de l'outillage retiré et non de votre code.
-DskipTests dans l'étape de build ?Parce que les tests appartiennent à la chaîne d'intégration continue, pas à la construction de l'image. Si vous les lancez ici, chaque docker build les rejoue — y compris sur le poste d'un collègue qui voulait seulement démarrer l'application.
Le bon découpage est celui-ci : Jenkins ou GitHub Actions exécute mvn verify, et ne construit l'image qu'une fois les tests verts. L'image devient alors l'artéfact d'un build déjà validé, et non l'endroit où l'on découvre qu'il est cassé.
Il reste un piège classique. Si votre Dockerfile commence par COPY . . sans .dockerignore, vous emportez dans le contexte de build le dossier target/, le dossier .git, et parfois node_modules. Le résultat : des couches inutiles, des builds lents, et des secrets qui se promènent.
Un .dockerignore minimal règle le problème :
target/
.git/
.gitignore
*.md
.env*Le build multi-étapes ne demande pas d'apprendre un nouvel outil : c'est un FROM supplémentaire et un COPY --from. En échange, l'image ne contient plus que ce qu'elle exécute — ce qui la rend plus petite, plus rapide à déployer, et sensiblement plus difficile à attaquer.
La question à se poser devant n'importe quel Dockerfile est toujours la même : est-ce que ce fichier sert à construire, ou à exécuter ? Tout ce qui répond « à construire » appartient à une étape jetable.
Une fois l'image allégée, reste à la faire tourner. C'est là que Docker s'arrête et que Kubernetes prend la suite : qui fait quoi entre les deux.
Développement et déploiement de solutions de données — les premiers modules sont en accès libre.