Docker les bases: image, container et layer
1. Vue d’ensemble
Nombreux sont les débutants en Docker qui confondent image et container, ou qui ne comprennent pas pourquoi docker build est si rapide lors de la deuxième exécution. Cet article explique trois concepts fondamentaux: image, container et layer — accompagnés d’un exemple réel de construction d’image pour une application Spring Boot. Après la lecture, vous serez capable d’écrire un Dockerfile sensé, de savoir comment exploiter le cache des layers pour construire plus rapidement, et d’éviter quelques erreurs communes lors de la conteneurisation d’une application Java.
2. Qu’est-ce qu’une image
Une image est un snapshot en lecture seule qui contient tout ce qu’il faut pour exécuter une application: le système d’exploitation de base, le runtime (la JVM par exemple), les bibliothèques, les variables d’environnement et votre code. Une image n’exécute pas d’elle-même — elle ressemble plutôt à un « modèle » à partir duquel créer des containers.
Vous définissez une image via un Dockerfile:
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/order-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Explication ligne par ligne:
FROM: sélectionne l’image de base (JRE 21 sur Temurin). Tout ce qui est construit par la suite repose sur cette base.WORKDIR: définit le répertoire de travail dans le container; les commandes suivantes s’exécutent relativement à ce répertoire.COPY: copie le fichier jar préalablement construit (issu demvn package) dans l’image.ENTRYPOINT: la commande exécutée au démarrage du container.
Construisez l’image avec la commande:
docker build -t order-service:1.0 .
Le résultat:
[+] Building 3.2s (8/8) FINISHED
=> [1/3] FROM eclipse-temurin:21-jre
=> [2/3] WORKDIR /app
=> [3/3] COPY target/order-service.jar app.jar
=> exporting to image
=> => naming to docker.io/library/order-service:1.0
L’image est maintenant sauvegardée localement; vous pouvez voir la liste avec docker images.
Remarque: Une image est simplement un fichier statique. Exécuter
docker buildplusieurs fois avec le même Dockerfile et le même code ne créera pas une application en cours d’exécution — pour cela, vous devez créer un container à partir de l’image.
3. Qu’est-ce qu’un container
Un container est une instance en exécution (ou arrêtée) créée à partir d’une image. Si une image est une « classe », un container est un « objet » — vous pouvez créer plusieurs containers à partir de la même image; chaque container est indépendant en termes de processus, réseau et couche d’écriture du système de fichiers.
Créer et lancer un container:
docker run -d --name order-svc -p 8080:8080 order-service:1.0
-d: exécute en mode détaché (arrière-plan).--name: donne un nom au container pour le gérer facilement, plutôt que d’utiliser un ID aléatoire.-p 8080:8080: mappe le port 8080 de l’hôte vers le port 8080 du container.
Vérifier les containers en cours d’exécution:
docker ps
CONTAINER ID IMAGE STATUS PORTS NAMES
3f2a9c1b8d4e order-service:1.0 Up 5 seconds 0.0.0.0:8080->8080/tcp order-svc
Vous pouvez lancer un second container à partir de la même image, en changeant simplement le nom et le port:
docker run -d --name order-svc-2 -p 8081:8080 order-service:1.0
Ces deux containers sont totalement indépendants: si l’un se bloque ou si vous écrivez des fichiers dans son système de fichiers, l’autre n’en sera pas affecté. L’image d’origine (order-service:1.0) reste inchangée — c’est la nature en lecture seule de l’image.
Arrêtez et supprimez le container quand il n’est plus utile:
docker stop order-svc && docker rm order-svc
3.1. Comment un container diffère-t-il d’une machine virtuelle (VM)
Un container partage le noyau avec l’hôte et ne virtualise pas le système d’exploitation complet comme une VM. Par conséquent, un container démarre en quelques secondes (comme le montre le log de construction ci-dessus) plutôt qu’en dizaines de secondes, et consomme beaucoup moins de ressources.
4. Qu’est-ce qu’une layer et pourquoi c’est important
Chaque ligne de commande dans un Dockerfile (FROM, COPY, RUN, ADD…) crée une layer distincte. Ces layers s’empilent (union filesystem), et Docker met en cache chaque layer — si une layer n’a pas changé depuis la dernière construction, Docker réutilise le cache au lieu de la reconstruire.
Considérez ce Dockerfile non optimisé:
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY . .
RUN mvn package -DskipTests
ENTRYPOINT ["java", "-jar", "target/order-service.jar"]
Le problème: COPY . . copie tout le code source, y compris si vous modifiez une seule ligne de commentaire. Comme la layer COPY change, Docker est forcé de reconstruire la layer RUN mvn package qui suit — ce qui prend plusieurs minutes à chaque fois, même si la logique du code est inchangée.
La façon d’optimiser est de séparer d’abord la partie dépendances (qui change rarement), puis la partie code (qui change souvent):
FROM eclipse-temurin:21-jdk AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=build /app/target/order-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
Explication:
COPY pom.xml .puisRUN mvn dependency:go-offline: cette layer change uniquement quand vous ajoutez/supprimez une dépendance danspom.xml. La plupart du temps, cette layer est mise en cache — en passant l’étape de téléchargement des bibliothèques (qui prend généralement le plus de temps).COPY src ./srcplacée après: seule cette layer et celles qui la suivent sont reconstruites quand vous modifiez le code; la layer de dépendances garde le cache.- Multi-stage build (
AS buildpuisFROM ... jre): l’étape finale ne récupère que le fichier jar construit, sans inclure le JDK ni le cache Maven — l’image de production est considérablement plus petite.
Consultez les layers d’une image avec:
docker history order-service:1.0
IMAGE CREATED CREATED BY SIZE
a1b2c3d4e5f6 3 minutes ago ENTRYPOINT ["java" "-jar"...] 0B
7f8e9d0c1b2a 3 minutes ago COPY /app/target/order-ser… 45.2MB
...
Chaque ligne correspond à une layer, avec sa taille. La layer COPY jar occupe la plus grande partie de l’espace — c’est logique puisqu’elle contient le code réel de l’application.
4.1. Comment fonctionne le cache de layers lors d’une reconstruction
Supposons que vous modifiez une seule classe Java puis que vous reconstruisez:
docker build -t order-service:1.1 .
=> [build 1/5] FROM eclipse-temurin:21-jdk
=> CACHED [build 2/5] COPY pom.xml .
=> CACHED [build 3/5] RUN mvn dependency:go-offline
=> [build 4/5] COPY src ./src
=> [build 5/5] RUN mvn package -DskipTests
=> [stage-1 2/2] COPY --from=build /app/target/order-service.jar app.jar
Les deux lignes CACHED montrent que Docker saute le téléchargement des dépendances — la construction est notablement plus rapide qu’avec l’approche COPY . . initiale.
Remarque: Docker compare le cache en fonction du contenu du fichier et de l’ordre des commandes, pas du timestamp. Si vous modifiez l’ordre d’une
COPYou d’unRUN, toutes les layers à partir de ce point seront invalidées, même si le contenu est réellement identique à la fois précédente.
5. Vérification par test automatisé
Lors de la conteneurisation d’un service, vous voulez généralement vous assurer que l’image construite s’exécute correctement. Utilisez la bibliothèque Testcontainers pour écrire des tests qui confirment que le container démarre et retourne les bonnes réponses.
Ajoutez la dépendance:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.1</version>
<scope>test</scope>
</dependency>
Écrivez le test:
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import static org.assertj.core.api.Assertions.assertThat;
class OrderServiceContainerTest {
@Test
void containerShouldRespondOnHealthEndpoint() {
try (GenericContainer<?> container =
new GenericContainer<>("order-service:1.0")
.withExposedPorts(8080)
.waitingFor(Wait.forHttp("/actuator/health"))) {
container.start();
assertThat(container.isRunning()).isTrue();
assertThat(container.getMappedPort(8080)).isPositive();
}
}
}
GenericContainercrée un container temporaire à partir de l’imageorder-service:1.0, se nettoie automatiquement quand le bloctryse termine.waitingFor(Wait.forHttp(...)): le test attend que l’endpoint health retourne un code 2xx avant de considérer le container comme prêt, évitant les conditions de course.getMappedPort(8080): Docker mappe un port aléatoire sur l’hôte; le test récupère le vrai port s’il doit faire d’autres appels.
Exécuter ce test confirme que l’image que vous avez construite à la section 2 non seulement se compile avec succès, mais s’exécute réellement correctement — une étape de vérification que beaucoup de gens oublient.
6. Erreurs courantes
- Confondre image et container lors du nettoyage:
docker rmsupprime un container, mais l’image reste et consomme toujours de l’espace disque. Pour supprimer une image, utilisezdocker rmi. - COPY toute la source avant d’installer les dépendances: comme l’explique la section 4, cela nuit à l’avantage du cache des layers et ralentit inutilement la construction.
- Utiliser une image de base trop lourde (par exemple
ubuntucomplète) pour un runtime JRE: l’image de production devient énorme, augmentant le temps de pull/déploiement. Préférez une image JRE minimaliste commeeclipse-temurin:21-jreou une variante-alpinesi compatible. - Construire une nouvelle image sans modifier le tag: réutiliser le tag
latestrend le rollback difficile en cas d’erreur en production, car vous ne savez pas exactement quelle image s’exécute.
Quand vous n’avez pas besoin de multi-stage build: si vous construisez le jar à l’avance en CI (par exemple GitHub Actions) puis ne faites que
docker build, vous n’avez pas besoin du stagebuilddans votre Dockerfile — contentez-vous deCOPYle jar déjà prêt, comme dans l’exemple de la section 2. Le multi-stage n’est nécessaire que si vous voulez que Docker compile le code source lui-même.
7. Conclusion
Une image est un modèle statique, un container est une instance en cours d’exécution créée à partir de cette image, et une layer est un ensemble de couches empilées qui aide Docker à mettre en cache intelligemment pour accélérer les constructions. Maîtriser l’ordre des layers dans votre Dockerfile — placer les éléments peu changeants (dépendances) avant les éléments qui changent fréquemment (code source) — est le moyen le plus simple d’optimiser le temps de construction. À partir de ces bases, vous pouvez progresser vers des sujets tels que Docker Compose pour orchestrer plusieurs services, ou optimiser la taille de l’image avec des bases distroless.