Si vous avez lu nos articles précédents, notamment notre guide ultime de Docker, vous savez maintenant qu’un conteneur est créé à partir d’une image Docker. On peut retrouver des milliers d’images sur Docker Hub, cependant, si l’on souhaite créer notre propre image ou apporter quelques modifications aux existantes, vous devez savoir construire votre propre image. Et le moyen d’y arriver est de maîtriser le fichier qui sert à le créer à savoir le dockerfile.

Dans ce mini tutoriel, nous allons vous aider dans cette démarche et vous donner le nécessaire afin que vous puissiez créer vos propres images dockers avec dockerfile.

Qu’est-ce qu’un dockerfile ?

Un dockerfile est un simple fichier texte, qui ne possède pas de format spécifique lors de sa création, contenant les instructions nécessaires afin de construire (build) une image docker. Ces instructions décrivent les actions que l’image doit exécuter une fois qu’elle sera créée. Elles seront lues une à une, puis exécutées indépendamment les unes des autres pour obtenir l’image souhaitée.

Il faut savoir qu’une image nouvellement créée est toujours issue d’une image de base. On va juste ajouter des fonctionnalités supplémentaires, une application par exemple, à celle-ci afin qu’elle puisse correspondre à nos attentes.

Comment fonctionne-t-il ?

Une image docker est construite en exécutant la commande docker build. Cette dernière exécutera les lignes de commande se trouvant dans le fichier dockerfile. En réalité, en lançant un docker build, le docker daemon lit chaque ligne se trouvant dans le dockerfile, puis réalise les requêtes demandées par celui-ci.

Pour que les commandes puissent être exécutées correctement, il faut que docker daemon puisse retrouver chaque fichier nécessaire à la construction de l’image. Il faut donc préciser dans le dockerfile l’emplacement où se trouvent ces derniers.

Cependant, lorsque docker daemon recherche les fichiers dont il a besoin, il parcourt tout le répertoire, ce qui peut entraîner une baisse de vitesse et de performance. De ce fait, il est préférable de ne placer que ce qu’il doit lire dans le chemin que vous avez mentionné. Toutefois, s’il existe quelques fichiers qui sont nécessaires à votre image, mais qui n’ont pas forcément besoin d’être parcourus, vous pouvez les énumérer dans un fichier qui s’appelle .dockerignore.

Présentation générale d’un dockerfile

Voici à quoi ressemble la structure d’un dockerfile :

#directive ou commentaire
INSTRUCTION arguments

Nous allons voir les commentaires et les directives dans la prochaine section, donc intéressons-nous plutôt sur la seconde ligne.

Les instructions font référence à des mots-clés spécifiques utilisés pour envoyer les requêtes à docker daemon. Il en existe une dizaine, nous allons voir la majeure partie d’entre elles un peu plus bas. Les arguments sont variables, car ce sera à vous de les définir selon vos besoins et l’objectif que vous souhaitez atteindre en construisant l’image.

Dockerfile n’est pas sensible à la casse, cependant, la bonne pratique est de mettre les instructions en majuscule afin de les différencier des arguments qu’elles prennent.

Les commentaires et les directives

Commentaires

Les commentaires dans un dockerfile doivent être précédés d’un #. Ils permettent, comme son nom l’indique de commenter, c’est-à-dire de donner plus de détails aux commandes que vous allez exécuter. Ces commentaires n’affectent pas l’exécution du dockerfile, car ils seront ignorés par docker daemon au moment où il va lire les instructions. On peut placer des commentaires n’importe où dans le fichier.

Directives

Les directives, quant à elles, permettent de spécifier la manière de gérer les instructions contenues dans le dockerfile. Elles sont toutefois optionnelles et ne peuvent être appelées qu’une seule fois. Une directive ne peut être appelée qu’une seule fois et doit impérativement se trouver au début du fichier. Elles commencent également par un #, donc docker daemon va les traiter en tant que commentaires si elles se situent autre part qu’au début de toutes les instructions.

Il existe deux types de directives que l’on appelle aussi des parser directives : syntax et escape. 

Syntax permet entre autres de spécifier la version de docker à utiliser. On peut par exemple, laisser l’image évoluer en la laissant se mettre à jour ou bien figer la version à utiliser.

#syntax=docker/dockerfile:1
#syntax=docker/dockerfile:1.1
#syntax=docker/dockerfile:1.2.1

Les exemples ci-dessus montrent trois manières différentes de spécifier une directive syntax. À la première ligne, la directive autorise le dockerfile à être mis à jour avec tous les dockers de version 1.x.x. Sur la deuxième ligne, il est autorisé à l’effectuer avec les versions 1.1.x et à la troisième ligne, la version est spécifiée en entier.

On peut également spécifier si l’on souhaite récupérer les versions stables ou labs (test). Pour ce faire, on ajoute  -labs pour les versions de test et rien pour les versions stables.

Pour la deuxième directive à savoir escape, il s’agit d’une directive permettant de spécifier le caractère pour définir les espaces. La valeur par défaut de celle-ci est \, mais on peut le modifier comme on le souhaite, comme ceci par exemple :

# escape=’

Les instructions de base

Une dizaine d’instructions peuvent être utilisées dans un dockerfile. Pour toutes les connaître, vous pouvez consulter la documentation officielle de docker. Ici, nous allons voir celles qui sont les plus couramment utilisées.

FROM

L’instruction FROM est celle que vous allez mettre au tout début, après les directives bien évidemment. C’est celle-ci qui sera exécutée en premier par le docker daemon. FROM sert à spécifier l’image de base que vous allez utiliser, image qui sera présente sur Docker Hub. Sans cela, l’image que vous souhaitez construire sera invalide, car c’est à partir de cette instruction qu’elle va être initialisée. Une seule commande peut être placée au-dessus d’une commande FROM à savoir ARG. Cette dernière ne sera pas prise en compte lors de la création de l’image, mais sert seulement à initialiser une variable nécessaire à l’initialisation de votre image à partir d’une image de base.

Voici un exemple d’utilisation de ces deux commandes dockerfile :

ARG VERSION=latest
FROM nginx
# suite des instructions

Dans la majeure partie des cas, on ne le retrouve qu’une seule fois dans un dockerfile. Cependant, lors de la création d’une application multiconteneur, on peut initialiser chaque image que l’on va utiliser à partir d’un seul dockerfile. Dans ce cas-ci, on peut voir deux ou plusieurs commandes FROM dans un seul fichier dockerfile.

ARG

Comme on peut le voir dans la section précédente, ARG permet de définir une variable utilisable au cours de l’exécution des instructions dans dockerfile. Si on la définit avant la commande FROM, elle ne sera pas prise en compte lors de la construction de l’image. Cependant, si elle se trouve en bas de FROM, docker daemon va l’exécuter en lui attribuant une valeur par défaut si celle-ci n’est pas spécifiée au moment du lancement du build.

ENV

ARG et ENV sont à peu près les mêmes commandes, car elles servent toutes à déclarer une variable. Cependant, ARG n’est disponible qu’au moment de l’exécution du dockerfile tandis que ENV peut être accessible même lorsque le conteneur créé par l’image sera lancé. Il s’agit de ce que l’on appelle une variable d’environnement, c’est-à-dire des variables qui sont nécessaires à l’exécution même du conteneur et de l’application. Par exemple, on déclare souvent les informations d’authentification à une base de données dans des variables d’environnement.

ENV VAR1=”value1”
# suite des instructions

WORKDIR

Cette commande permet de spécifier le répertoire dans lequel seront basées les instructions qui suivront son appel. Toutes les commandes correspondantes au build de l’image à savoir RUN, ADD et COPY seront donc affectées au chemin mentionné dans WORKDIR. Il en va de même des commandes nécessaires à l’exécution du conteneur à savoir EXPOSE, CMD et ENTRYPOINT.

Le répertoire par défaut de l’exécution des instructions est le répertoire actuel à savoir /. On peut définir plusieurs WORKDIR tout le long du dockerfile. Si aucune commande n’est spécifiée, WORKDIR sera automatiquement créé par Docker.

WORKDIR /app
# suite des instructions

Dans l’exemple ci-dessus, nous spécifions que les instructions qui suivent seront basées sur le répertoire /app.

USER

Tout comme WORKDIR, USER permet d’initialiser un contexte spécifique pour l’exécution des commandes qui le suit. Ici, USER sert à déterminer l’utilisateur ou le groupe d’utilisateur pouvant interagir avec l’image qui sera créée.

USER user1
# suite des instructions

Si aucune commande USER n’est spécifié, celles qui suivront seront exécutées avec l’utilisateur root. Mais si l’on décide d’y attribuer une valeur, il faut que cet utilisateur existe, donc il faut le créer à l’aide de la commande :

net user /add 

ADD

L’instruction ADD permet de copier un fichier ou un dossier venant d’un répertoire interne ou externe vers un chemin de destination contenant le système de fichiers de l’image. Généralement, il s’agit du code source et des dépendances de l’application que l’on va faire tourner dans le conteneur. La syntaxe de base pour utiliser cette commande est la suivante :

ADD  

Il y existe cependant plusieurs façons de spécifier la destination du contenu à ajouter :

  • De manière relative :
ADD test.txt path/

Ici, on ajoute le fichier test.txt à la destination spécifiée dans WORKDIR en ajoutant le chemin mentionné sur la destination. En réalité, le chemin absolu de la destination est <valeur_WORKDIR>/path/

  • De manière absolue :
ADD test.txt /path/

Dans ce cas-ci, le fichier test.txt sera directement ajouté au chemin path/.

COPY

COPY et ADD agissent de la même manière, mais contrairement à ADD, COPY ne permet pas d’importer des documents venant d’une source distante telle qu’une URL. Dans la plupart des cas, on utilise COPY afin d’éviter des désagréments causés par l’utilisation de liens externes autorisés par ADD.

On peut exécuter ces commandes, que ce soit COPY ou ADD, plusieurs fois au sein d’un dockerfile. Sa syntaxe est similaire à celle de ADD à savoir :

COPY  

Si l’on ne spécifie pas la destination, le fichier ou le dossier sera copié à la racine du système de fichier de l’image créée.

RUN

La commande RUN permet d’exécuter des commandes supplémentaires à l’intérieur du build du dockerfile. L’argument qu’elle prend est identique à une commande Shell ordinaire. On peut donc s’en servir afin de télécharger et d’installer les dépendances nécessaires à l’application ou encore à directement afficher un résultat ou un message. Voici la syntaxe de base de la commande :

RUN 

Ici, on exécute une commande qui affiche ‘hello world’.

On peut également utiliser RUN sous la forme exec. Dans ce cas, au lieu de spécifier l’argument comme une commande Shell, on utilise la notation suivante :

RUN ["executable", "param1", "param2"]

On utilise ce format surtout lorsque l’image de base ne possède pas d’exécutable Shell.

Pour éviter que l’image s’alourdisse, on peut spécifier plusieurs commandes à l’intérieur de l’argument d’une seule commande RUN.

EXPOSE

Cette commande permet de rediriger l’exécution du conteneur qui sera lancé à partir de l’image créée par le dockerfile vers un port prédéfini. C’est sur le port qui sera mentionné que le conteneur sera accessible lorsqu’une commande docker run sera exécutée. Cependant, le port exposé à l’aide de la commande EXPOSE peut être écrasé en utilisant la commande docker run -p. EXPOSE supporte les protocoles TCP et UDP, la syntaxe de base est celle-ci :

EXPOSE /

On peut par exemple spécifier que le conteneur est exposé sur le port 80 en utilisant cette commande :

EXPOSE 80/tcp

CMD et ENTRYPOINT

Ces deux commandes servent à mentionner les instructions qui seront exécutées en premier au moment du lancement du conteneur créé à partir de l’image obtenue avec le dockerfile. Ce qui différencie ces deux commandes est que CMD permet d’exécuter une action sans avoir besoin de paramètres supplémentaires tandis que ENTRYPOINT est inchangeable et exécute la même action tout le long de l’activation du conteneur. Dans ce cas, il agit comme un fichier exécutable.

Voici un exemple d’instructions utilisant ces commandes :

ENTRYPOINT [‘echo’, ‘hello’, ‘world’]
CMD [echo ‘hello world’]

Ces deux commandes donnent les mêmes résultats, c’est pour cela que, parfois, on n’utilise qu’une seule d’entre elles, malgré le fait que l’on peut tout à fait les utiliser ensemble.

Les layers de dockerfile

Lorsqu’on construit notre propre image, en réalité, on ajoute une couche éditable au-dessus de l’image docker de base. Donc, lorsque l’on exécute un dockerfile, on ajoute toujours une nouvelle couche. Cela peut être embarrassant et peut alourdir l’image lorsque l’on modifie et que l’on reconstruit notre image.

C’est pour remédier à cela que docker a mis en place un système de caches pour la construction de l’image que l’on nomme communément layers. Ce système permet de ne plus effectuer le build des instructions non modifiées et ainsi d’accélérer ce processus.

Créer une image à partir d’un dockerfile

Dans cette partie, nous allons vous donner un mini-tutoriel qui crée une image d’un petit serveur web Python. Avant de commencer, il faut que vous ayez Python 3 installé sur votre machine. Il faut aussi disposer d’un éditeur de texte et Docker bien sûr.

Créer le web server Python

Pour le web serveur, commencez par créer un dossier nommé docker-python (vous pouvez aussi le nommer comme vous le souhaitez). Ensuite, lancer une invite de commande et copiez les commandes suivantes pour commencer l’installation du web server :

pip3 install Flask
$ pip3 freeze | findstr Flask >> requirements.txt
flask-python dockerfile

Ces commandes vont installer Flask qui est un framework de développement Python puis créer un fichier nommé requirement.txt qui va contenir la version installée de Flask.

flask-requirements dockerfile

Lorsque ces commandes sont lancées, les packages existants seront automatiquement téléchargés.

Une fois que l’exécution de cette commande est terminée, créer un fichier nommé app.py dans le dossier dans lequel vous vous trouvez. Dans ce fichier, vous allez copier les instructions suivantes qui vont être celles de l’application : 

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Bonjour de Data Transition Numérique !'
app-python

Note : les commandes ci-dessus sont valables pour Windows. Si vous utilisez Linux, remplacez findstr par grep. Vous pouvez également créer le fichier app.py en utilisant la commande touch app.py

Vous pouvez tester votre application en lançant la commande suivante :

python -m flask run

puis en tapant sur l’URL http://127.0.0.1:5000/

docker image

Créer le dockerfile

Une fois que nous avons notre application, nous allons nous occuper du dockerfile. Pour cela, créez un fichier sans extension nommé dockerfile dans notre dossier docker-python puis ajouter les instructions suivantes :

# syntax=docker/dockerfile:1

FROM python:3.8-slim-buster

WORKDIR /app

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

COPY . .

EXPOSE 5000

CMD [ "python3", "-m" , "flask", "run", "--host=0.0.0.0"]
dockerfile

Ici, nous utilisons les commandes dockerfile suivantes :

  • Le directive syntax pour spécifier la version de docker à utiliser et les mises à jour autorisées ;
  • FROM : pour indiquer l’image de base que nous souhaitons utiliser à savoir Python 3.8 ;
  • WORKDIR : pour communiquer l’emplacement dans laquelle souhaitons-nous que les commandes qui suivent se basent ;
  • COPY pour copier les fichiers nécessaires à l’exécution de l’image à savoir requirements.txt ;
  • Nous allons ensuite installer les packages nécessaires à l’image à l’aide de la commande RUN. Ici, nous avons demandé à docker daemon d’aller puiser dans le fichier texte copié ci-dessus le nom des packages à installer à savoir Flask ;
  • Après cela, nous copions tout vers le système de fichier de l’image à créer à l’aide de la commande COPY . . ;
  • Nous allons indiquer le port 8000 que nous allons utiliser avec la commande PORT ;
  • La dernière ligne spécifie l’action à exécuter en premier lors du lancement du conteneur. Il s’agit de la même commande que nous avons utilisée pour tester notre application dans la section précédente.

Et voilà, nous avons le dockerfile qui va nous servir à créer notre propre image.

Exécuter le dockerfile

Maintenant que nous avons notre dockerfile, nous pouvons construire notre image en utilisant la commande suivante :

docker build --tag python-docker .
docker-build

Vous voyez que docker daemon lance une à une les commandes présentes dans le dockerfile. Une fois cette exécution terminée, vérifiez si l’image a bien été créée avec cette commande :

docker images
docker-images

Comme nous pouvons le constater, notre image a bien été créée.

Si nous supprimons cette image en utilisant la commande docker rmi <ID_de_l_image> et qu’on le reconstruit, nous allons voir que certaines commandes ont été mises en cache et ne sont plus exécutées.

docker_build

Pour vérifier que l’on peut accéder à notre application, nous allons lancer un conteneur de l’image avec la commande suivante :

docker run -p 8000:5000 python-docker

Ici, nous avons indiqué que l’application exposée au port 5000, c’est-à-dire notre application, sera accessible via le port 8000. Vérifions cela en tapant http://127.0.0.1:8000/ sur un navigateur.

localhost-docker

Voilà, nous arrivons à la fin de ce mini-tutoriel. Vous savez maintenant créer votre propre image docker à l’aide d’un dockerfile. Cela va vous permettre de mieux aborder ce composant docker mais également de maîtriser le reste des composants, car dockerfile est à la base de presque tout le concept de conteneurisation docker.

Si vous souhaitez approfondir vos connaissances en programmation informatique et en Big Data, nous vous invitons à visiter notre blog et à télécharger cette formation sur la programmation Scala.

>