⚠️ Traduction non officielle - Cette documentation est une traduction communautaire non officielle de Docker.

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 ou ENTRYPOINT) 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.

  1. Téléchargez et installez Docker Desktop.

  2. Ouvrez ce projet pré-initialisé pour générer un fichier ZIP. Voici à quoi cela ressemble :

    Une capture d'écran de l'outil Spring Initializr sélectionné avec Java 21, Spring Web et Spring Boot 3.4.0

    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.

  3. 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épertoire src/test/java contient la source de test, et le fichier pom.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.

  4. 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 fichier SpringBootDockerApplication.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 package com.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.

  1. Créez un fichier nommé Dockerfile dans le même dossier qui contient tous les autres dossiers et fichiers (comme src, pom.xml, etc.).

  2. Dans le Dockerfile, définissez votre image de base en ajoutant la ligne suivante :

    FROM eclipse-temurin:21.0.2_13-jdk-jammy
  3. 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
  4. 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 ./
  5. 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
  6. Copiez le répertoire src de votre projet sur la machine hôte vers le répertoire /app dans le conteneur.

    COPY src ./src
  7. 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'objectif spring-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

  1. Exécutez la commande suivante pour construire l'image Docker :

    $ docker build -t spring-helloworld .
    
  2. 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

  1. 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)
  2. 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

  1. 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 fine eclipse-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.

  2. 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 votre Dockerfile situé dans le répertoire actuel.

    Note

    Dans 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 commande docker build, Docker construira automatiquement la dernière étape par défaut. Vous pourriez utiliser docker build -t spring-helloworld-builder --target builder . pour construire uniquement l'étape builder avec l'environnement JDK.

  3. 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.

Ressources supplémentaires