Constructions multi-étapes
Explication
Dans une construction traditionnelle, toutes les instructions de construction sont exécutées en séquence, et dans un seul conteneur de construction : téléchargement des dépendances, compilation du code et packaging de l'application. Toutes ces couches se retrouvent dans votre image finale. Cette approche fonctionne, mais elle conduit à des images volumineuses portant un poids inutile et augmentant vos risques de sécurité. C'est là qu'interviennent les constructions multi-étapes.
Les constructions multi-étapes introduisent plusieurs étapes dans votre Dockerfile, chacune avec un objectif spécifique. Pensez-y comme à la capacité d'exécuter différentes parties d'une construction dans plusieurs environnements différents, simultanément. En séparant l'environnement de construction de l'environnement d'exécution final, vous pouvez réduire considérablement la taille de l'image et la surface d'attaque. Ceci est particulièrement bénéfique pour les applications avec de grandes dépendances de construction.
Les constructions multi-étapes sont recommandées pour tous les types d'applications.
- Pour les langages interprétés, comme JavaScript, Ruby ou Python, vous pouvez construire et minifier votre code dans une étape, et copier les fichiers prêts pour la production vers une image d'exécution plus petite. Cela optimise votre image pour le déploiement.
- Pour les langages compilés, comme C, Go ou Rust, les constructions multi-étapes vous permettent de compiler dans une étape et de copier les binaires compilés dans une image d'exécution finale. Pas besoin d'inclure tout le compilateur dans votre image finale.
Voici un exemple simplifié d'une structure de construction multi-étapes utilisant du pseudo-code. Notez qu'il y a plusieurs instructions FROM
et un nouveau AS <nom-etape>
. De plus, l'instruction COPY
dans la deuxième étape copie --from
l'étape précédente.
# Étape 1 : Environnement de construction
FROM builder-image AS build-stage
# Installer les outils de construction (ex. Maven, Gradle)
# Copier le code source
# Commandes de construction (ex. compiler, packager)
# Étape 2 : Environnement d'exécution
FROM runtime-image AS final-stage
# Copier les artefacts d'application depuis l'étape de construction (ex. fichier JAR)
COPY --from=build-stage /chemin/dans/etape/construction /chemin/vers/placement/dans/etape/finale
# Définir la configuration d'exécution (ex. CMD, ENTRYPOINT)
Ce Dockerfile utilise deux étapes :
- L'étape de construction utilise une image de base contenant les outils de construction nécessaires pour compiler votre application. Elle inclut des commandes pour installer les outils de construction, copier le code source et exécuter les commandes de construction.
- L'étape finale utilise une image de base plus petite adaptée à l'exécution de votre application. Elle copie les artefacts compilés (un fichier JAR, par exemple) depuis l'étape de construction. Enfin, elle définit la configuration d'exécution (en utilisant
CMD
ouENTRYPOINT
) pour démarrer votre application.
Essayez-le
Dans ce guide pratique, vous découvrirez la puissance des constructions multi-étapes pour créer des images Docker légères et efficaces pour un exemple d'application Java. Vous utiliserez une simple application "Hello World" basée sur Spring Boot construite avec Maven comme exemple.
-
Téléchargez et installez Docker Desktop.
-
Ouvrez ce projet pré-initialisé pour générer un fichier ZIP. Voici à quoi cela ressemble :
Spring Initializr est un générateur de démarrage rapide pour les projets Spring. Il fournit une API extensible pour générer des projets basés sur JVM avec des implémentations pour plusieurs concepts communs — comme la génération de langage de base pour Java, Kotlin et Groovy.
Sélectionnez Générer pour créer et télécharger le fichier zip pour ce projet.
Pour cette démonstration, vous avez associé l'automatisation de construction Maven avec Java, une dépendance Spring Web et Java 21 pour vos métadonnées.
-
Naviguez dans le répertoire du projet. Une fois que vous décompressez le fichier, vous verrez la structure de répertoire de projet suivante :
spring-boot-docker ├── HELP.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main │ ├── java │ │ └── com │ │ └── example │ │ └── spring_boot_docker │ │ └── SpringBootDockerApplication.java │ └── resources │ ├── application.properties │ ├── static │ └── templates └── test └── java └── com └── example └── spring_boot_docker └── SpringBootDockerApplicationTests.java 15 répertoires, 7 fichiers
Le répertoire
src/main/java
contient le code source de votre projet, le répertoiresrc/test/java
contient la source de test, et le fichierpom.xml
est le modèle d'objet de projet (POM) de votre projet.Le fichier
pom.xml
est le cœur de la configuration d'un projet Maven. C'est un fichier de configuration unique qui contient la plupart des informations nécessaires pour construire un projet personnalisé. Le POM est énorme et peut sembler intimidant. Heureusement, vous n'avez pas encore besoin de comprendre chaque subtilité pour l'utiliser efficacement. -
Créez un service web RESTful qui affiche "Hello World!".
Sous le répertoire
src/main/java/com/example/spring_boot_docker/
, vous pouvez modifier votre fichierSpringBootDockerApplication.java
avec le contenu suivant :package com.example.spring_boot_docker; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class SpringBootDockerApplication { @RequestMapping("/") public String home() { return "Hello World"; } public static void main(String[] args) { SpringApplication.run(SpringBootDockerApplication.class, args); } }
Le fichier
SpringbootDockerApplication.java
commence par déclarer votre packagecom.example.spring_boot_docker
et importer les frameworks Spring nécessaires. Ce fichier Java crée une application web Spring Boot simple qui répond avec "Hello World" quand un utilisateur visite sa page d'accueil.
Créer le Dockerfile
Maintenant que vous avez le projet, vous êtes prêt à créer le Dockerfile
.
-
Créez un fichier nommé
Dockerfile
dans le même dossier qui contient tous les autres dossiers et fichiers (comme src, pom.xml, etc.). -
Dans le
Dockerfile
, définissez votre image de base en ajoutant la ligne suivante :FROM eclipse-temurin:21.0.2_13-jdk-jammy
-
Maintenant, définissez le répertoire de travail en utilisant l'instruction
WORKDIR
. Cela spécifiera où les commandes futures s'exécuteront et le répertoire où les fichiers seront copiés à l'intérieur de l'image de conteneur.WORKDIR /app
-
Copiez à la fois le script wrapper Maven et le fichier
pom.xml
de votre projet dans le répertoire de travail actuel/app
dans le conteneur Docker.COPY .mvn/ .mvn COPY mvnw pom.xml ./
-
Exécutez une commande dans le conteneur. Elle exécute la commande
./mvnw dependency:go-offline
, qui utilise le wrapper Maven (./mvnw
) pour télécharger toutes les dépendances de votre projet sans construire le fichier JAR final (utile pour des constructions plus rapides).RUN ./mvnw dependency:go-offline
-
Copiez le répertoire
src
de votre projet sur la machine hôte vers le répertoire/app
dans le conteneur.COPY src ./src
-
Définissez la commande par défaut à exécuter lorsque le conteneur démarre. Cette commande instruit le conteneur d'exécuter le wrapper Maven (
./mvnw
) avec l'objectifspring-boot:run
, qui construira et exécutera votre application Spring Boot.CMD ["./mvnw", "spring-boot:run"]
Et avec cela, vous devriez avoir le Dockerfile suivant :
FROM eclipse-temurin:21.0.2_13-jdk-jammy WORKDIR /app COPY .mvn/ .mvn COPY mvnw pom.xml ./ RUN ./mvnw dependency:go-offline COPY src ./src CMD ["./mvnw", "spring-boot:run"]
Construire l'image de conteneur
-
Exécutez la commande suivante pour construire l'image Docker :
$ docker build -t spring-helloworld .
-
Vérifiez la taille de l'image Docker en utilisant la commande
docker images
:$ docker images
Cela produira une sortie comme la suivante :
REPOSITORY TAG IMAGE ID CREATED SIZE spring-helloworld latest ff708d5ee194 3 minutes ago 880MB
Cette sortie montre que votre image fait 880 MB. Elle contient le JDK complet, la chaîne d'outils Maven, et plus encore. En production, vous n'avez pas besoin de cela dans votre image finale.
Exécuter l'application Spring Boot
-
Maintenant que vous avez une image construite, il est temps d'exécuter le conteneur.
$ docker run -p 8080:8080 spring-helloworld
Vous verrez alors une sortie similaire à la suivante dans le journal du conteneur :
[INFO] --- spring-boot:3.3.4:run (default-cli) @ spring-boot-docker --- [INFO] Attaching agents: [] . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v3.3.4)
-
Accédez à votre page "Hello World" via votre navigateur à http://localhost:8080, ou via cette commande curl :
$ curl localhost:8080 Hello World
Utiliser les constructions multi-étapes
-
Considérez le Dockerfile suivant :
FROM eclipse-temurin:21.0.2_13-jdk-jammy AS builder WORKDIR /opt/app COPY .mvn/ .mvn COPY mvnw pom.xml ./ RUN ./mvnw dependency:go-offline COPY ./src ./src RUN ./mvnw clean install FROM eclipse-temurin:21.0.2_13-jre-jammy AS final WORKDIR /opt/app EXPOSE 8080 COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
Notez que ce Dockerfile a été divisé en deux étapes.
-
La première étape reste la même que le Dockerfile précédent, fournissant un environnement de développement Java (JDK) pour construire l'application. Cette étape est donnée le nom de builder.
-
La seconde étape est une nouvelle étape nommée
final
. Elle utilise une image plus fineeclipse-temurin:21.0.2_13-jre-jammy
, contenant juste l'environnement d'exécution Java (JRE) nécessaire pour exécuter l'application. Cette image fournit un environnement d'exécution Java (JRE) qui est suffisant pour exécuter l'application compilée (fichier JAR).
Pour une utilisation en production, il est fortement recommandé de produire un runtime personnalisé JRE utilisant jlink. Les images JRE sont disponibles pour toutes les versions d'Eclipse Temurin, mais
jlink
vous permet de créer un runtime minimal contenant uniquement les modules Java nécessaires pour votre application. Cela peut réduire considérablement la taille et améliorer la sécurité de votre image finale. Consultez cette page pour plus d'informations.Avec les constructions multi-étapes, une construction Docker utilise une image de base pour la compilation, le packaging et les tests unitaires et puis une image distincte pour l'exécution de l'application. En conséquence, l'image finale est plus petite en taille puisqu'elle ne contient aucun outil de développement ou de débogage. En séparant l'environnement de construction de l'environnement d'exécution final, vous pouvez réduire considérablement la taille de l'image et augmenter la sécurité de vos images finales.
-
-
Reconstruisez maintenant votre image et exécutez votre build prêt pour la production.
$ docker build -t spring-helloworld-builder .
Cette commande construit une image Docker nommée
spring-helloworld-builder
en utilisant la dernière étape de votreDockerfile
situé dans le répertoire actuel.NoteDans votre Dockerfile multi-étape, la dernière étape (final) est la cible par défaut pour la construction. Cela signifie que si vous ne spécifiez pas explicitement une étape cible en utilisant le drapeau
--target
dans la commandedocker build
, Docker construira automatiquement la dernière étape par défaut. Vous pourriez utiliserdocker build -t spring-helloworld-builder --target builder .
pour construire uniquement l'étape builder avec l'environnement JDK. -
Comparez la différence de taille d'image en utilisant la commande
docker images
:$ docker images
Vous obtiendrez une sortie similaire à la suivante :
spring-helloworld-builder latest c5c76cb815c0 24 minutes ago 428MB spring-helloworld latest ff708d5ee194 About an hour ago 880MB
Votre image finale est de 428 MB, comparée à la taille de construction d'origine de 880 MB.
En optimisant chaque étape et en n'incluant que ce qui est nécessaire, vous avez été en mesure de réduire considérablement la taille globale de l'image tout en atteignant la même fonctionnalité. Cela ne n'améliore pas seulement les performances mais rend également vos images Docker plus légères, plus sûres et plus faciles à gérer.