if [ -n "$CI" ]; then
echo "⏭ Skipping Maven build in CI"
else
./mvnw package -Pprod
java -jar target/hello-java-*.jar app.App1
fi⏭ Skipping Maven build in CI
Créer et optimiser des images Docker
La correction des exercices est disponible dans le dépôt :
✅ : ebpro/sample-containers-exercices-images
/home/jovyan/work/github/ebpro/sample-containers-exercices-imagesClone Command:
git clone -b develop https://github.com/ebpro/sample-containers-exercices-imagesCré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 :
src/main/java/app/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 25
- 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>25</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>
<!-- Analyse SonarQube et couverture JaCoCo (profil "sonar") -->
<sonar.maven.plugin.version>5.5.0.6356</sonar.maven.plugin.version>
<jacoco.maven.plugin.version>0.8.14</jacoco.maven.plugin.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>
<!-- ================================================= -->
<!-- Profil SonarQube (activé avec -Psonar) -->
<!-- Analyse SonarQube + couverture JaCoCo. -->
<!-- Variables requises : SONAR_URL, SONAR_TOKEN -->
<!-- ================================================= -->
<profile>
<id>sonar</id>
<properties>
<sonar.host.url>${SONAR_URL}</sonar.host.url>
<sonar.token>${SONAR_TOKEN}</sonar.token>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.maven.plugin.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<goals>
<goal>report</goal>
</goals>
<configuration>
<formats>
<format>XML</format>
</formats>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>${sonar.maven.plugin.version}</version>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>Générer le Maven Wrapper avec la commande :
mvn wrapper:wrapperTester que l’application fonctionne localement :
if [ -n "$CI" ]; then
echo "⏭ Skipping Maven build in CI"
else
./mvnw package -Pprod
java -jar target/hello-java-*.jar app.App1
fi⏭ Skipping Maven build in CI
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:25-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-*.jarif [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker build -t hello-java:0.0.1 .
fi⏭ Skipping Docker step in CI
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker run --label teaching-demo=true --rm hello-java:0.0.1
fi⏭ Skipping Docker step in CI
ARG, 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 :
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker build \
-f Dockerfile.arg_env \
--build-arg GIT_COMMIT=abc123 \
-t hello-java:env \
.
fi⏭ Skipping Docker step in CI
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker run --label teaching-demo=true --rm hello-java:env
fi⏭ Skipping Docker step in CI
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker run --label teaching-demo=true --rm --env LOG_LEVEL=DEBUG hello-java:env
fi⏭ Skipping Docker step in CI
ENV COMMIT=${GIT_COMMIT} ?Dockerfile.arg_env
FROM eclipse-temurin:25-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
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker build \
--file Dockerfile.multistage \
-t hello-java:0.0.1-multi \
.
fi⏭ Skipping Docker step in CI
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker run --label teaching-demo=true --rm hello-java:0.0.1-multi
fi⏭ Skipping Docker step in CI
if [ -n "$CI" ]; then
echo "⏭ Skipping Docker step in CI"
else
docker run --label teaching-demo=true --rm -e NAME=Pierre hello-java:0.0.1-multi
fi⏭ Skipping Docker step in CI
docker image ls --filter "reference=hello-java*"i Info → U In Use IMAGE ID DISK USAGE CONTENT SIZE EXTRA
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:25-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:25-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"]