Dockerfile Java : une image légère, sûre et rapide

· 7 min de lecture

Comment écrire un bon Dockerfile pour une application Java. Au programme : build en plusieurs étapes, couches du JAR, utilisateur sans droits root et mémoire de la JVM.

En bref

Une application Java peut être rangée dans une « image » pour s’installer partout de la même façon. L’article montre comment rendre cette image plus petite, plus sûre et plus rapide à installer.

Un mot technique vous échappe ? Le glossaire du développement web l’explique simplement.

Un Dockerfile de trois lignes qui copie le JAR dans une image JDK fonctionne… mais produit une image de plusieurs centaines de Mo, entièrement retransférée à chaque modification du code. Quelques ajustements suffisent pour obtenir une image plus légère, plus sûre et plus rapide à déployer.

Séparer la compilation de l’exécution

Un build multi-étapes compile avec un JDK complet, puis ne garde qu’un JRE dans l’image finale. Maven, les sources et le cache de dépendances restent dans l’étape de build :

FROM eclipse-temurin:21-jdk AS build
WORKDIR /build
COPY .mvn/ .mvn/
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline -q
COPY src/ src/
RUN ./mvnw package -DskipTests -q && cp target/*.jar application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

FROM eclipse-temurin:21-jre
WORKDIR /app
RUN useradd --system --uid 1001 spring
USER spring
COPY --from=build /build/extracted/dependencies/ ./
COPY --from=build /build/extracted/spring-boot-loader/ ./
COPY --from=build /build/extracted/snapshot-dependencies/ ./
COPY --from=build /build/extracted/application/ ./
ENTRYPOINT ["java", "-jar", "application.jar"]

Pourquoi découper le JAR en couches ?

Docker met en cache chaque instruction COPY. Les dépendances changent rarement, le code de l’application à chaque commit. En les copiant séparément, un nouveau déploiement ne transfère plus que la couche application, souvent quelques centaines de kilo-octets au lieu de toutes les librairies. Le mode jarmode=tools est disponible depuis Spring Boot 3.3.

Ne pas tourner en root

Par défaut, le processus d’un conteneur s’exécute en root. Un utilisateur dédié, créé avec useradd puis activé par USER, limite les dégâts en cas de faille dans l’application ou dans l’une de ses dépendances.

Régler la mémoire de la JVM

La JVM détecte la limite mémoire du conteneur, mais par défaut elle ne réserve au heap qu’un quart de cette mémoire. Pour une application seule dans son conteneur, c’est souvent trop peu :

ENV JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=75"

Inutile d’aller jusqu’à 100 % : le Metaspace, les piles des threads et les tampons réseau consomment aussi de la mémoire en dehors du heap. Les threads virtuels (voir « Virtual Threads Java : la fin du réactif obligatoire ? ») font exception : leur pile est stockée dans le heap.

Et sans Dockerfile ?

Spring Boot sait aussi construire une image avec les Cloud Native Buildpacks : ./mvnw spring-boot:build-image produit une image en couches, qui tourne sans root, sans écrire une ligne de Dockerfile. Moins de contrôle, mais un excellent point de départ.

Un .dockerignore pour un build plus rapide

Avant chaque build, Docker envoie tout le dossier du projet au moteur de build. Sans .dockerignore, cela inclut l’historique Git, les fichiers de l’IDE et les anciens artefacts de compilation. Les exclure accélère le build et évite d’invalider le cache pour rien :

target/
.git/
.idea/
*.iml
.vscode/

Enfin, fixez la version exacte de l’image de base : par exemple eclipse-temurin:21-jre plutôt que latest. Les builds deviennent reproductibles. Une nouvelle version de l’image ne peut plus changer votre application sans que personne ne l’ait décidé.

En résumé

  • Un build multi-étapes : JDK pour compiler, JRE pour exécuter.
  • Les couches du JAR extraites pour profiter du cache Docker.
  • Un utilisateur non root.
  • MaxRAMPercentage ajusté à la limite du conteneur.
  • Un .dockerignore qui exclut target/, .git et les fichiers de l’IDE.

Sur le même sujet

  • Java
  • Spring Boot
  • Concurrence

Virtual Threads Java : la fin du réactif obligatoire ?

Depuis Java 21, les threads virtuels permettent d’écrire du code bloquant qui passe à l’échelle. Faut-il abandonner WebFlux pour autant ?

· 9 min de lecture