Pratique : Images Docker

Créer et optimiser des images Docker

Practice
Containers
Docker
Images
Exercices pratiques pour créer et optimiser des images Docker avec des applications Java simples.
Auteur
Affiliations

Université de Toulon

LIS UMR CNRS 7020

Date de publication

2026-11-07

ImportantCorrections

La correction des exercices est disponible dans le dépôt :

✅ : ebpro/sample-containers-exercices-images

Détails
  • URL: https://github.com/ebpro/sample-containers-exercices-images
  • Branch: develop
  • Local Path: /home/jovyan/work/github/ebpro/sample-containers-exercices-images
  • Latest Commit: 58e9ae6 - chore: add _quarto-utils submodule + .devcontainer (SOTA pattern)
  • Timestamp: 2026-11-07T19:33:28Z

Clone Command:

git clone -b develop https://github.com/ebpro/sample-containers-exercices-images

EXERCICE 1 — Image Java simple

Créer une image Docker contenant une application Java simple construite avec Maven Wrapper.

Structure du projet

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
package app;

import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

public class App1 {
    private static final Logger logger = LoggerFactory.getLogger(App1.class);

    public static void main(String[] args) {
        logger.info("Hello from Java in Docker!");
    }
}

Etudier le fichier pom.xml, regarder :

  • les coordonnées du projet (groupId, artifactId, version) : identifier ton application et sa version.
  • la section properties est très importante, car elle regroupe les versions de Java et des bibliothèques ; le projet utilise Java 21.
    • la propriété 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.
  • La partie dependencies définit les bibliothèques externes dont le projet a besoin, comme JUnit pour les tests ou les outils de logging pour afficher des messages.
  • build/plugins, définissent comment Maven va compiler le code et lancer les tests automatiquement.
  • Enfin, la section profiles (notamment prod). Un profil Maven est une partie optionnelle de la configuration activé manuellement (ici avec -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:wrapper

Tester 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
ImportantA FAIRE
  1. Ecrire un Dockerfile pour construire l’image Docker. Vous utiliserez l’image de base eclipse-temurin:21-jdk-alpine. Vous devrez copier les fichiers, construire l’application avec Maven Wrapper, et définir la commande de démarrage.
  2. Construire l’image Docker
  3. Lancer un conteneur pour exécuter l’application Java.
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-*.jar
if [ -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

Exercice 2 — 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);
    }
}
ImportantA FAIRE

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
  1. Lancez le conteneur pour vérifier que le commit est bien passé au build :
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
  1. Lancez le conteneur en surchargeant avec une variable d’environnement le niveau de logs (runtime) :
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
  1. Que peut-on faire si on ajoute dans le Dockerfile une instruction 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

EXERCICE 3 — Multi-stage build + ENTRYPOINT/CMD

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 + "!");
    }
}
ImportantA FAIRE

Créer une image optimisée en multi-stage et utiliser ENTRYPOINT + CMD.

  1. Ecrire un Dockerfile Dockerfile.multistage qui utilise une construction en deux étapes :

    • une étape de build utilisant l’image eclipse-temurin:21-jdk pour compiler l’application Java
    • une étape runtime utilisant l’image eclipse-temurin:21-jre pour exécuter cette nouvelle classe de l’application Java
  2. Utiliser 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
  1. Lancer le conteneur sans argument (doit afficher “Hello World!”)
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
  1. Lancer le conteneur avec un argument (doit afficher “Hello Pierre!”)
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
  1. comparer la taille de l’image optimisée
docker image ls --filter "reference=hello-java*"
                                                            i Info →   U  In Use

IMAGE   ID             DISK USAGE   CONTENT SIZE   EXTRA
ImportantExtensions (Bonus)
  1. (Bonus 1) Modifier l’application Java pour ajouter une variable d’environnement GREETING personnalisable pour le message de salutation.
  2. (Bonus 2) Optimiser le Dockerfile pour le pas télécharger inutilement des dépendances Maven à chaque build quand seule le code source change.
  3. (Bonus 3) Utiliser --mount=type=cache associé à RUN pour cacher le répertoire .m2 lors du build Maven.
  4. (Bonus 4) Utiliser des images de base plus légères (ex: 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"]

Réutilisation