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 :
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.
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.
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.
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.
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.
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.
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 !
Gestion des Queues et Topics sous Jboss 7. @MessageDriven et configuration
Vous démarrez un projet d’application et voulez mettre en place un outil d’intégration continue pour votre projet ? On vous partage nos conseils et notre retour d’expérience sur le sujet !
Découvrez la planche #40 !