./mvnw --quiet --batch-mode package -Pprod
java -jar target/hello-world-*.jar app.App1Error: Unable to access jarfile target/hello-world-*.jar
: 1
Créer et optimiser des images Docker
2026-02-01
Jupyter Kernel: bash
🖥️ Env Ubuntu 24.04.3 LTS / x86_64 • 🐳 Docker Client 29.1.5 / Server 29.1.5 • 🌿 Git Branch @ 7b914bd
Ce support a été généré par Quarto : les cellules sont exécutées par le noyau indiqué lors du rendu.
Certaines parties ont été rédigées avec l’assistance d’un modèle de langage ; le contenu a été relu et validé par l’auteur.
✅ Updated existing repository: ebpro/notebook-containers-intro-exercices-images
Créer une image Docker contenant une application Java simple construite avec Maven Wrapper.
Créez l’arborescence suivante :
hello-java/
├── src/
│ └── main/
│ └── java/
│ └── app/
│ └── App1.java
├── pom.xml
Etudier le code de l’application Java App1.java :
Etudier le fichier pom.xml, regarder :
main.class définit la classe principale de l’application (avec la méthode main) qui sera définit dans le manifeste du JAR exécutable.-Pprod). Ce profile sert à créer une version prête à être distribuée, avec un JAR exécutable (dans target) et les dépendances copiées dans le dossier (libe).pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
POM Maven pédagogique
=====================
Objectif :
- Compiler une application Java 21
- Exécuter des tests
- Produire un JAR exécutable
- Copier les dépendances dans target/libs
- Être facilement utilisable avec Docker
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- Version du modèle Maven (toujours 4.0.0) -->
<modelVersion>4.0.0</modelVersion>
<!-- Coordonnées du projet -->
<groupId>fr.univtln.bruno.demos.docker</groupId>
<artifactId>hello-java</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>Hello Java</name>
<description>
Application Java "Hello World" utilisée pour démontrer Maven et Docker
</description>
<!-- ===================================================== -->
<!-- Propriétés -->
<!-- ===================================================== -->
<!--
Bonne pratique :
- Centraliser toutes les versions
- Faciliter les mises à jour
- Éviter les valeurs "en dur"
-->
<properties>
<!-- Encodage -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Version de Java -->
<java.version>21</java.version>
<!-- Classe principale de l'application -->
<main.class>app.App1</main.class>
<!-- Dépendances -->
<junit.version>5.11.4</junit.version>
<slf4j.version>2.0.16</slf4j.version>
<logback.version>1.5.16</logback.version>
<!-- Plugins Maven -->
<maven.compiler.version>3.13.0</maven.compiler.version>
<maven.surefire.version>3.5.2</maven.surefire.version>
<maven.jar.version>3.4.2</maven.jar.version>
<maven.dependency.version>3.6.1</maven.dependency.version>
<versions.maven.version>2.21.0</versions.maven.version>
</properties>
<!-- ===================================================== -->
<!-- Dépendances -->
<!-- ===================================================== -->
<dependencies>
<!-- ======================= -->
<!-- Dépendances de test -->
<!-- ======================= -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- ======================= -->
<!-- Logging (runtime) -->
<!-- ======================= -->
<!-- API de logging (abstraction) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<!-- Implémentation concrète -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
</dependencies>
<!-- ===================================================== -->
<!-- Build -->
<!-- ===================================================== -->
<build>
<plugins>
<!-- ======================= -->
<!-- Compilation Java -->
<!-- ======================= -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<!-- Utilisation du mode release (bonne pratique) -->
<release>${java.version}</release>
<!-- Conserve les noms des paramètres (utile pour frameworks) -->
<parameters>true</parameters>
</configuration>
</plugin>
<!-- ======================= -->
<!-- Exécution des tests -->
<!-- ======================= -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.version}</version>
</plugin>
<!-- ======================= -->
<!-- Gestion des versions -->
<!-- ======================= -->
<plugin>
<!-- Plugin de maintenance (NON exécuté automatiquement) -->
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>${versions.maven.version}</version>
<configuration>
<rulesUri>
https://bruno.univ-tln.fr/rules.xml
</rulesUri>
</configuration>
</plugin>
</plugins>
</build>
<!-- ===================================================== -->
<!-- Profils Maven -->
<!-- ===================================================== -->
<!--
Un profil = un mode de build
Ici :
- default : développement
- prod : production / Docker
-->
<profiles>
<!-- ================================================= -->
<!-- Profil production -->
<!-- ================================================= -->
<profile>
<id>prod</id>
<build>
<plugins>
<!-- ========================================= -->
<!-- Copier les dépendances dans target/libs -->
<!-- ========================================= -->
<!--
Résultat :
target/
├── hello-java-XXX.jar
└── libs/
├── logback.jar
└── slf4j.jar
-->
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<version>${maven.dependency.version}</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/libs
</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
<!-- ========================================= -->
<!-- Création du JAR exécutable -->
<!-- ========================================= -->
<!--
Le MANIFEST.MF contiendra :
- Main-Class
- Class-Path: libs/...
-->
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>${maven.jar.version}</version>
<configuration>
<archive>
<manifest>
<mainClass>${main.class}</mainClass>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
</manifest>
<!-- Infos utiles pour le debug -->
<manifestEntries>
<Built-By>${user.name}</Built-By>
<Build-Jdk>${java.version}</Build-Jdk>
<Build-Time>${maven.build.timestamp}</Build-Time>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>Les options -ntp --quiet permettent de réduire la verbosité de Maven dans les logs sur ce support. L’option --batch-mode réponnd automatiquement “oui” aux questions posées par Maven.
supprimer ces options lors de l’exécution.
Générer le Maven Wrapper avec la commande :
Tester que l’application fonctionne localement :
Error: Unable to access jarfile target/hello-world-*.jar
: 1
eclipse-temurin:21-jdk-alpine. Vous devrez copier les fichiers, construire l’application avec Maven Wrapper, et définir la commande de démarrage.Dockerfile
# Définir l'image de base
FROM eclipse-temurin:21-jdk
# Definir le répertoire de travail
WORKDIR /app
# Copier les fichiers du projet dans le conteneur
COPY . .
# Construire l'application avec Maven Wrapper
RUN ./mvnw package -Pprod
# Définir la commande de démarrage
# Il est plus propre d'utiliser la syntaxe json comme nous le ferons plus tard
# mais pour faire simple ici nous utilisons la syntaxe shell qui support les les variables d'environnement et le globbing (*)
CMD java -jar target/hello-java-*.jarARG, ENV (adapté au projet hello-java)Ajouter au projet la classe app.App2 qui lit un fichier pour connaitre le commit. Ce fichier sera créé à la construction de l’image avec un build-arg. L’application lit aussi la variable d’environnement LOG_LEVEL (pour contrôler le niveau de logs au runtime).
src/main/java/app/App2.java
package app;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import java.nio.file.Files;
import java.nio.file.Path;
public class App2 {
private static final Logger logger = LoggerFactory.getLogger(App2.class);
public static void main(String[] args) {
try {
String gitCommit = Files.readString(Path.of("/app/GIT_COMMIT")).trim();
logger.info("Git commit (build-time) : " + gitCommit);
} catch (Exception e) {
logger.error("Failed to read GIT_COMMIT file", e);
}
String logLevel = System.getenv("LOG_LEVEL");
logger.info("Log level (run-time) : " + logLevel);
}
}Créer un Dockerfile.arg_env qui accepte un build-arg et crée le fichier /app/GIT_COMMIT avec cette valeur.
Depuis le dossier hello-java/, construisez l’image en passant la valeur du commit au build :
sha256:03f9ec828e4d6415f0fc2a5540809e9d7c39eaec9afe9edb762c87a15f642338
07:47:12.023 [main] INFO app.App2 -- Git commit (build-time) : abc123
07:47:12.025 [main] INFO app.App2 -- Log level (run-time) : INFO
07:47:13.011 [main] INFO app.App2 -- Git commit (build-time) : abc123
07:47:13.013 [main] INFO app.App2 -- Log level (run-time) : DEBUG
ENV COMMIT=${GIT_COMMIT} ?Dockerfile.arg_env
FROM eclipse-temurin:21-jdk
WORKDIR /app
# Définir une variable d’argument pour le commit Git
ARG GIT_COMMIT="unknown"
# Pour rendre la variable d’argument disponible en tant que variable d’environnement
# Il est alors possible de la surcharger au runtime avec --env
# ENV GIT_COMMIT=${GIT_COMMIT}
# inscrire la valeur dans un fichier dans l'image qui pourra être lue par l'application
RUN printf '%s' "${GIT_COMMIT}" > /app/GIT_COMMIT
# Ajouter un label avec le commit Git dans les métadonnées de l’image
LABEL git.commit="${GIT_COMMIT}"
# Configuration dynamique du conteneur
ENV LOG_LEVEL=INFO
COPY . .
RUN ./mvnw package -Pprod
CMD java -cp target/hello-java-*.jar app.App2
Compléter l’application Java :
src/main/java/app/App3.java
package app;
import java.util.Optional;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
public class App3 {
private static final Logger logger = LoggerFactory.getLogger(App3.class);
public static void main(String[] args) {
String name = Optional.ofNullable(System.getenv("NAME")).orElse("World");
logger.info("Hello " + name + "!");
}
}Créer une image optimisée en multi-stage et utiliser ENTRYPOINT + CMD.
Ecrire un Dockerfile Dockerfile.multistage qui utilise une construction en deux étapes :
eclipse-temurin:21-jdk pour compiler l’application Javaeclipse-temurin:21-jre pour exécuter cette nouvelle classe de l’application JavaUtiliser ENTRYPOINT pour définir la commande principale et CMD pour passer un argument optionnel (le nom à saluer).
Tester votre solution avec les commandes suivantes :
Construire l’image Docker
sha256:01b25219280cfaa7a0a95b493509b7bcdeb1754ff6da5233e8b7c60d9a0533b9
07:48:07.499 [main] INFO app.App3 -- Hello Pierre!
i Info → U In Use IMAGE ID DISK USAGE CONTENT SIZE EXTRA hello-java:0.0.1 b875dd92269f 776MB 255MB hello-java:0.0.1-multi 01b25219280c 404MB 101MB hello-java:env 03f9ec828e4d 776MB 255MB
GREETING personnalisable pour le message de salutation.--mount=type=cache associé à RUN pour cacher le répertoire .m2 lors du build Maven.eclipse-temurin:21-jre-alpine).Dockerfile.multistage
# =========================
# Étape 1 : build Maven
# =========================
# Image de base avec JDK (nécessaire pour compiler)
FROM eclipse-temurin:21-jdk AS build
# Répertoire de travail dans le conteneur
WORKDIR /app
# Copier uniquement les fichiers Maven (pour optimiser le cache Docker)
COPY pom.xml mvnw ./
COPY .mvn .mvn
# Télécharger les dépendances dans le cache Maven
# (cache persistant entre builds grâce à BuildKit)
RUN --mount=type=cache,target=/root/.m2 \
./mvnw -q dependency:go-offline
# Copier le code source (après les dépendances pour optimiser le cache)
COPY src src
# Compiler le projet et générer :
# - le JAR de l'application
# - le dossier libs/ contenant les dépendances
RUN --mount=type=cache,target=/root/.m2 \
./mvnw package -Pprod -DskipTests
# =========================
# Étape 2 : image runtime
# =========================
# Image plus légère avec uniquement le JRE
FROM eclipse-temurin:21-jre
WORKDIR /app
# Copier le JAR principal depuis l'étape build
COPY --from=build /app/target/hello-java-*.jar app.jar
# Copier les dépendances dans un dossier libs/
COPY --from=build /app/target/libs libs
# Script d'entrée
COPY entrypoint.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
# Best practice: run as non-root user
RUN useradd -m appuser
USER appuser
# Variable d'environnement exemple
ENV NAME=World
# Lancer l'application avec un classpath composé :
# - du JAR principal
# - de toutes les libs dans le dossier libs/
ENV MAIN_CLASS=app.App3
ENTRYPOINT ["/app/entrypoint.sh"]