Comment optimiser vos images Docker ?

On vous explique différentes méthodes pour optimiser vos images Docker : temps de build, taille de l'image et bonnes pratiques pour éviter les effets de bords.
Nathan.jpg
Nathan MITTELETTE, M. propre du codeMis à jour le 13 Avr 2023
optimiser image docker tuto

De plus en plus utilisé sur le marché, Docker est devenu un outil indispensable pour le déploiement d’application sur des environnements partagés. Il est donc de plus en plus important de savoir comment bien réaliser son Dockerfile pour ainsi obtenir la meilleure image Docker possible.

Cet article explique en détail des fonctionnalités de Docker ce qui nécessite une bonne connaissance des bases de Docker. Si ce n’est pas le cas, nous vous conseillons cet article d'introduction sur le sujet.

Dans cet article, nous allons vous dévoiler les différentes méthodes que l’on utilise pour optimiser nos Dockerfile. Cette optimisation s’applique à 3 niveaux différents :

  1. Au niveau du temps de build de notre image
  2. Au niveau de la taille de l’image
  3. Ainsi que l’utilisation des bonnes pratiques pour éviter les effets de bords

Méthode 1 - Optimiser le temps de build de notre image

Les deux solutions que nous utilisons pour optimiser le temps de build de notre image sont une bonne gestion du cache ainsi que l’utilisation de différentes stages.

La gestion du cache

Docker utilise un système de cache qui permet d’accélérer le build d’une image Docker. Pour ce faire, chaque ligne du Dockerfile qui est exécutée est mise en cache et devient un layer.

Grâce à ce mécanisme, lorsque Docker exécute une ligne du Dockerfile, si jamais cette ligne est déjà présente dans le cache et qu’elle ne contient aucune modification, il récupère directement le layer présent dans le cache sans avoir besoin d’exécuter la ligne.

Docker détecte un changement pour une ligne si l’état de l’image Docker, avant l’exécution de la ligne, a changé (si un fichier est modifié par exemple).

De ce fait, si jamais un changement a été repéré sur une ligne du dessus, alors Docker ne va plus utiliser le cache jusqu’à la fin du build de l’image.

Comment l’utiliser à notre avantage ?

  • Mettre les lignes qui vont connaître le plus de changements le plus bas possible du Dockerfile. De ce fait, cela permet aux lignes du dessus d’être directement récupérées du cache.
  • Regrouper certaines instructions (les lignes qui contiennent des exécutions de commande) sur une seule ligne pour réduire la taille du cache car ce dernier contient moins de layer.

Méthode 2 - Utiliser des stages

Qu’est-ce qu’une stage dans un Dockerfile

Les stages au sein d’un Dockerfile permettent de regrouper certaines instructions. De ce fait, lors du build d’une image Docker, il est à la fois possible de compiler son application ainsi que de préparer l’environnement dans lequel l’application compilée va être exécutée. Avec un exemple c’est plus parlant :

# Stage de Build
FROM maven:3.6.3-jdk-11 AS build
ENV MYAPP_HOME /opt/backend
WORKDIR $MYAPP_HOME
# Récupération des sources pour effectuer le build de l'application au sein du Dockerfile
COPY pom.xml .
COPY src ./src
RUN mvn package -DskipTests

# Stage de Run
# C'est cette stage qui défini le contenu de l'image une fois le build terminé
FROM openjdk:11-jre
ENV MYAPP_HOME /opt/backend
WORKDIR $MYAPP_HOME

# Récupération de l'application build dans la stage du dessus pour l'intégrer dans la stage de Run
COPY --from=build $MYAPP_HOME/target/*.jar $MYAPP_HOME/app.jar

# On expose le port de l'application
EXPOSE 8080

# On spécifie la commande à lancer au démmarage de l'application
ENTRYPOINT [ "java -jar app.jar" ]

Cet exemple se base sur la conteneurisation d’une application JavaLangage de développement très populaire !, on retrouve ainsi 2 stages, la stage de build ainsi que la stage de run.

L’utilisation des stages nous permet d’utiliser 2 images différentes à 2 étapes de notre Dockerfile. Une image qui contient un JDK (qui permet de build les applications Java) ainsi qu’une image qui contient un JRE (qui permet de lancer les applications Java). De ce fait, nous allons pouvoir lors de la première stage, avoir tous les outils pour build l’application, ensuite transférer cette application dans la stage suivant qui elle contient tous les outils pour lancer l’application.

On peut donc partir directement du code, le compiler puis le lancer, tout ça lors du build de l’image Docker. De plus, dans la finalité, l’image Docker ne va contenir que les outils pour lancer l’application. En effet, les images des stages précédentes ne sont pas gardées, seuls les éléments que l’on récupère des stages précédentes sont gardés ainsi que la dernière image présente.

Comment l’utiliser à notre avantage ?

Il est important que la dernière étape (ou stage) possède l’image la plus légère possible, c’est cette dernière qui sert de base pour définir la taille totale de votre image Docker.

Il est également important de savoir que les stages au sein d’un Dockerfile, peuvent être exécutées en parallèle. Cela peut vous permettre d’accélérer le temps de build de vos Dockerfile. Cependant, dès lors qu’une stage à besoin d’une autre (par exemple la récupération de l’application une fois qu’elle est build) l’exécution de cette dernière est mise en pause en attendant que la stage attendue ait fini d’être exécutée.

Il est donc important, si l’on souhaite gagner du temps lors du build des images Docker, de mettre les lignes qui ont des dépendances avec d’autres stages le plus bas possible dans l’exécution de la stage.

Méthode 3 - Optimiser la taille de notre image Docker

De manière globale, on souhaite que l’image Docker soit la plus légère possible. En effet, cette image est souvent stockée sur un hub (hub.docker.com) et est téléchargée sur votre serveur pour être lancée. Si l’on souhaite optimiser cette action, il est alors important d’avoir une image Docker la plus légère possible.

La taille d’une image Docker dépend de la taille des différentes layer qui la compose. On appelle layer, une instruction de votre Dockerfile. De ce fait, si on ajoute un fichier dans un layer et qu’on le supprime dans un layer inférieur, on retrouvera malgré tout la taille de ce fichier au sien de la taille globale de l’image Docker car il est présent dans le layer du dessus.

En se basant sur ce principe, si jamais un fichier est créé dans un layer et déplacé dans un autre, on retrouvera 2 fois la taille du fichier dans la taille de l’image Docker car il est présent dans deux layers différents.

Voici un exemple se basant sur des installations avec apt pour illustrer ces propos :

# Installation de PHP 
# LAYER 1
RUN apt update
# LAYER 2
RUN apt install -y php7.4-cli
# LAYER 3
RUN rm -rf /var/lib/apt/lists/*

Dans ce cas, au sein du premier layer, on met à jour les sources de apt ce qui créé des fichiers au sein du dossier /var/lib/apt/lists. On remarque que ces fichiers sont supprimés dans le layer 3.

Par défaut, comme les fichiers sont présent dans le layer 1 ils seront présents dans la taille globale de l’image Docker. Pour résoudre ce problème il faut effectuer ces différentes actions en une seule ligne de commande :

# Installation de PHP 
# LAYER 1
RUN apt update && apt install -y \
    php7.4-cli \
    && rm -rf /var/lib/apt/lists/*

En effectuant toutes ces actions en une seule ligne, il est possible d’installer PHPLangage de programmation s’exécutant côté serveur et permettant la création dynamique de pages web ou d'APIs. sur notre image sans perdre de la place avec les fichiers présents dans le dossier /var/lib/apt/lists.

Pour explorer la taille de vos différentes couches Docker ainsi que ce qu’elle contiennent, nous vous conseillons d’utiliser l’outil Dive.

Les bonnes pratiques

Utilisation d’un utilisateur dans vos images

Par défaut, lorsque l’on build une image docker, les commandes vont être exécutées avec un utilisateur root. Pour des questions de sécurité, on préfère que l’utilisateur qui lance notre application ne possède pas tous les droits, mais uniquement ceux nécessaires pour le bon déroulement de l’application.

Pour ce faire il est nécessaire de créer un utilisateur au sein de votre Dockerfile et de lui donner les droits nécessaires pour exécuter votre application. Vous pouvez utiliser le mot-clé USER dans votre Dockerfile pour spécifier à Docker avec quel utilisateur il faut lancer le container.

Vérifier les numéros de version

Par défaut, Docker lorsqu’il doit récupérer une image, si aucun tag n’est spécifié, il récupère le tag latest. En général, il est préférable de spécifier une version spécifique d’un tag. En effet, le tag latest spécifie la dernière version de l’image, sauf que cette version elle évolue au fur et à mesure du temps. De ce fait, si jamais on build 2 fois la même image, avec 2 jours d’intervalle, on n’est pas sûr d’avoir le même résultat car le contenu de l’image latest peut avoir changé. Et comme on ne souhaite pour faire des monter de version de nos images de base sans le vouloir, on préfère spécifier un tag avec une version spécifique.

Il est également important de faire attention aux images que l’on souhaite utiliser et de leurs provenances. Il est préférable d’utiliser des images créées par des instances connues.

Au sein des différents tags d’image, il existe quelques conventions. En général on essaye de récupérer une image légère, de ce fait on préfère utiliser des images qui contiennent les mots-clefs alpine, light ou encore slim.

Vous avez toutes les cartes en main pour optimiser vos images Docker ! Pour poursuivre l'échange, n'hésitez pas à nous suivre ou à nous contacter !