Le logiciel open-source Docker s’est imposé comme la norme dans la vir­tua­li­sa­tion basées sur les con­te­neurs. La vir­tua­li­sa­tion basée sur les con­te­neurs constitue la prochaine étape dans l’évolution des machines vir­tuelles, mais avec une dif­fé­rence sig­ni­fi­ca­tive. Au lieu de simuler un système d’ex­ploi­ta­tion complet, une ap­pli­ca­tion unique est vir­tua­li­sée dans un conteneur. De nos jours, les con­te­neurs Docker sont utilisés dans toutes les phases du cycle de vie du software, tels que le dé­ve­lop­pe­ment, les tests et l’ex­ploi­ta­tion.

Il existe dif­fé­rents concepts dans l’éco­sys­tème Docker. La con­nais­sance et la com­pré­hen­sion de ces com­po­sants est es­sen­tielle pour tra­vail­ler ef­fi­ca­ce­ment avec Docker. En par­ti­cu­lier, on retrouve parmi ces derniers les images Docker, les con­te­neurs Docker, et les Do­cker­files. Nous vous ex­pli­quons le fonc­tion­ne­ment et donnons des conseils pratiques d’uti­li­sa­tion.

Qu’est-ce qu’un Do­cker­file ?

Un Do­cker­file constitue l’unité de base au sein de l’éco­sys­tème Docker. Il décrit les étapes de la création d’une image Docker. Le flot d’in­for­ma­tions suit ce modèle central : Do­cker­file > image Docker > conteneur Docker.

Un conteneur Docker dispose d’une durée de vie limitée et interagit avec son en­vi­ron­ne­ment. Imaginez que le conteneur est un organisme uni­cel­lu­laire, une cellules de levure, par exemple. Suivant cette analogie, une image Docker cor­res­pond peu ou prou aux in­for­ma­tions gé­né­tiques. Tous les con­te­neurs créés à partir d’une image unique sont iden­tiques, de même que tous les or­ga­nismes uni­cel­lu­laires sont clonés à partir des mêmes in­for­ma­tions gé­né­tiques. Dès lors, quel rôle jouent les Do­cker­files dans ce modèle ?

Un Do­cker­file définit les étapes pour créer une nouvelle image. Il est important que vous com­pre­niez que tout commence par une image de base existante. L’image nou­vel­le­ment créée succède à l’image de base. Il y a également un certain nombre de chan­ge­ments spé­ci­fiques. Si nous revenons à notre exemple de cellule de levure, les chan­ge­ments cor­res­pon­dent à des mutations. Un Do­cker­file spécifie deux choses pour une nouvelle image Docker :

  1. L’image de base à partir de laquelle la nouvelle image est dérivée. Ceci ancre la nouvelle image dans l’arbre gé­néa­lo­gique de l’éco­sys­tème Docker.
  2. Un certain nombre de chan­ge­ments spé­ci­fiques qui dis­tin­guent la nouvelle image de l’image de base.

Comment fonc­tionne un Do­cker­file et comment créer une image à partir de celui-ci ?

Un Do­cker­file n’est ni plus ni moins qu’un fichier texte normal. Le Do­cker­file contient un ensemble d’ins­truc­tions, chacune présente sur une ligne séparée. Les ins­truc­tions sont exécutées l’une après l’autre pour créer une image Docker. Vous avez peut-être entendu parler de cette idée à l’occasion de l’exécution d’un script de trai­te­ment par lots. Au cours de l’exécution, des couches sup­plé­men­taires sont ajoutées à l’image étape par étape. Nous vous ex­pli­quons exac­te­ment comment cela fonc­tionne dans notre article consacré aux images Docker.

Une image Docker est créée en exécutant les ins­truc­tions présentes dans un Do­cker­file. Cette étape s’intitule le build process (« processus de dé­ve­lop­pe­ment » en français) et est en­clen­chée en exécutant la commande « docker build ». Le « build context » (« contexte de dé­ve­lop­pe­ment » en français) est un concept central. Ce dernier définit à quels fichiers et ré­per­toires le build process a accès. Ici, un ré­per­toire local fait office de source. Le sommaire du ré­per­toire source est passé au Docker Daemon lorsque la commande « docker build » est appelée. Les ins­truc­tions dans le Do­cker­file accèdent aux fichiers et ré­per­toires dans le build context.

Il peut arriver que vous ne vouliez pas inclure tous les fichiers présent dans le dossier source dans le build context. Vous pouvez utiliser le fichier .do­cke­rig­nore à cette fin. Ce dernier est utilisé pour exclure des fichiers et ré­per­toires du build context. Le nom est emprunté au fichier .gitignore de Git. Le point initial dans le nom du fichier indique qu’il s’agit d’un fichier caché.

Comment un Do­cker­file est-il structuré ?

Un Do­cker­file constitue un fichier en texte brut intitulé « Do­cker­file ». Veuillez noter que la première lettre doit être mise en capitale. Le fichier contient une entrée par ligne. Voici la structure générale d’un Do­cker­file :

# Commentaire
INSTRUCTION arguments

Outre les com­men­taires, les Do­cker­files con­tien­nent des ins­truc­tions et des arguments. Ces derniers décrivent la structure de l’image.

Com­men­taires et di­rec­tives parser

Les com­men­taires con­tien­nent des in­for­ma­tions destinées d’abord à des humains. Par exemple, les com­men­taires présents au sein d’un Do­cker­file com­men­cent avec un signe dièse (#) dans Python, Perl et Ruby. Les lignes de com­men­taires sont sup­pri­mées durant le build process avant d’aller plus loin dans le process. Veuillez noter que seules les lignes qui com­men­cent avec un signe dièse sont reconnues comme des lignes de com­men­taire.

Voici un com­men­taire valide :

# notre image de base
FROM busybox

Par contraste, il existe une erreur ci-dessous étant donné que le signe dièse ne se situe pas au début de la ligne :

FROM busybox # notre image de base

Les di­rec­tives Parser cons­ti­tuent un type spé­ci­fique de com­men­taire. Elles sont situées dans les lignes de com­men­taire et doivent se situer au début du Do­cker­file. Autrement, elles seront traitées comme des com­men­taires et sup­pri­mées durant le dé­ve­lop­pe­ment. Il est également important de noter qu’une directive parser donnée peut uni­que­ment être utilisée une fois dans un Do­cker­file.

Au moment de l’écriture de cet article, il existe seulement deux types de di­rec­tives parser : « syntax » et « escape ». La directive parser « escape » définit le symbole espace à utiliser. Ceci est utilisé pour écrire des ins­truc­tions sur plusieurs lignes, de même que pour exprimer des ca­rac­tères spé­ci­fiques. La directive parser « syntax » spécifie les règles aux­quelles le parser doit avoir recours pour procéder les ins­truc­tions du Do­cker­file. Voici un exemple :

# syntax=docker/Dockerfile:1
# escape=\

Ins­truc­tions, arguments et variables

Les ins­truc­tions cons­ti­tuent la majorité du contenu du Do­cker­file. Les ins­truc­tions décrivent la structure spé­ci­fique d’une image Docker et sont exécutées les unes après les autres. À l’instar des commandes sur la ligne de commande, les ins­truc­tions mo­bi­li­sent des arguments. Certaines ins­truc­tions sont di­rec­te­ment com­pa­rables à des commandes de ligne de commandes spé­ci­fiques. Dès lors, il existe une ins­truc­tion COPY qui copie les fichiers et ré­per­toires et équivaut peu ou prou à la commande cp sur la ligne de commande. Quoi qu’il en soit, une dif­fé­rence avec la ligne de commande est que certaines ins­truc­tions Do­cker­file ont des règles spé­ci­fiques pour leur séquence. Par ailleurs, certaines ins­truc­tions peuvent uni­que­ment ap­pa­raître une fois dans un Do­cker­file.

Note

Les ins­truc­tions n’ont pas à être obli­ga­toi­re­ment mise en capitales. Il est quand même con­seil­ler de suivre la con­ven­tion lorsque vous créez un Do­cker­file.

En ce qui concerne les arguments, vous devez établir une dis­tinc­tion entre les parties codées en dur et variables. Docker suit la mé­tho­do­lo­gie 12 facteurs et utilise des variables d’en­vi­ron­ne­ment pour con­fi­gu­rer des con­te­neurs. L’ins­truc­tion ENV est utilisée pour définir les variables d’en­vi­ron­ne­ment dans un Do­cker­file. À présent, jetons un œil à la manière dont on assigne une valeur à la variable d’en­vi­ron­ne­ment.

Les valeurs stockées dans les variables d’en­vi­ron­ne­ment peuvent être lues et utilisées en tant que parties variables des arguments. Une syntaxe spé­ci­fique est utilisée à cette fin. Cela évoque les scripts shell. Le nom de la variable d’en­vi­ron­ne­ment est précédé par un signe dollar : $env_var. Il existe également une notation al­ter­na­tive pour délimiter ex­pli­ci­te­ment le nom de la variable dans laquelle ce dernier est placé entre des accolades : ${env_var}. Jetons un œil à un exemple concret :

# donner à la variable 'user' la valeur 'admin'
ENV user="admin"
# définir le nom d’utilisateur comme 'admin_user'
USER ${user}_user

Les ins­truc­tion Do­cker­file les plus im­por­tantes

Nous allons main­te­nant vous présenter les ins­truc­tions Do­cker­file les plus im­por­tantes. Tra­di­tion­nel­le­ment, certaines ins­truc­tions, notamment FROM, n’étaient au­to­ri­sées à ap­pa­raître qu’une seule fois dans Do­cker­file. Quoi qu’il en soit, il existe main­te­nant des multi-stage builds. Ils décrivent plusieurs images dans un Do­cker­file. La res­tric­tion s’applique alors à chaque stade de build in­di­vi­duel.

Ins­truc­tion Des­crip­tion Com­men­taire
FROM Définir l’image de base Doit ap­pa­raître comme la première ins­truc­tion ; une seule entrée par stade de build
ENV Définit les variables d’en­vi­ron­ne­ment pour le build process et le container runtime
ARG Déclare les pa­ra­mètres de ligne de commande pour le build process Peut ap­pa­raître avant l’ins­truc­tion FROM
WORKDIR Modifie le ré­per­toire actuel
USER Modifie le statut de membre de l’uti­li­sa­teur et de groupe
COPY Copie les fichiers et ré­per­toires sur l’image Crée une nouvelle couche
ADD Copie les fichiers et ré­per­toires sur l’image Crée une nouvelle couche ; uti­li­sa­tion dé­con­seil­lée
RUN Exécute la commande dans l’image durant le build process Crée une nouvelle couche
CMD Définit les arguments par défaut pour le lancement du conteneur Une seule entrée par stade de build
EN­TRY­POINT Définit la commande par défaut pour le lancement du conteneur Une seule entrée par stade de build
EXPOSE Définit les as­sig­na­tions de port pour le conteneur exécuté Les ports doivent être exposés lors du lancement du conteneur
VOLUME Inclut le ré­per­toire dans l’image en tant que volume lors du lancement du conteneur dans le système hôte

Ins­truc­tion FROM

L’ins­truc­tion FROM définit l’image de base sur laquelle les ins­truc­tions ul­té­rieures opèrent. Cette ins­truc­tion ne peut exister qu’une fois par stade de build et doit ap­pa­raître dans la première ins­truc­tion. Attention : l’ins­truc­tion ARG peut ap­pa­raître avant l’ins­truc­tion FROM. Vous pouvez ainsi spécifier exac­te­ment quelle image est utilisée en tant qu’image de base via un argument de ligne de commande lorsque vous lancez le build process.

Chaque image Docker doit être basée sur une image de base. En d’autres termes, chaque image Docker possède exac­te­ment une image parent. Ceci conduit au dilemme de l’œuf et de la poule. La lignée doit bien commencer quelque part. Dans l’univers Docker, la lignée commence avec l’image « scratch ». Cette image minimale tient lieu d’origine de toute image Docker.

Note

En anglais, « from scratch » signifie que quelque chose est fabriqué à partir d’in­gré­dients de base. Ce terme est utilisé en bou­lan­ge­rie et en cuisine. Si un fichier Docker commence par la ligne « FROM scratch », cela signifie que l’image est assemblée à partir de rien.

Ins­truc­tions ENV et ARG

Ces deux ins­truc­tions assignent une valeur à une variable. La dis­tinc­tion entre les deux ins­truc­tions réside prin­ci­pa­le­ment dans l’origine des valeurs et le contexte dans lequel les variables sont dis­po­nibles. Com­men­çons par jeter un œil à l’ins­truc­tion ARG.

L’ins­truc­tion ARG déclare une variable dans le Do­cker­file qui est uni­que­ment dis­po­nible au cours du build process. La valeur d’une variable déclarée avec ARG est passée en tant qu’argument de ligne de commande lorsque le build process est lancé. Voici un exemple dans lequel nous déclarons la variable de build « user » :

ARG user

Lorsque nous lançons le build process, nous passons la valeur réelle de la variable :

docker build --build-arg user=admin

Lorsque vous déclarez la variable, vous pouvez choisir de spécifier une valeur par défaut. Si un argument con­ve­nable n’est pas passé lorsque vous lancez le build process, la valeur par défaut est attribuée à la variable :

ARG user=tester

Sans avoir recours à « --build-arg », la variable « user » contient la valeur par défaut « tester » :

docker build

Nous dé­fi­nis­sons ici une variable d’en­vi­ron­ne­ment à l’aide de l’ins­truc­tion ENV. Con­trai­re­ment à l’ins­truc­tion ARG, une variable définie avec ARG existe à la fois lors du build process et au cours de l’exécution du conteneur. L’ins­truc­tion ENV peut être rédigée de deux manières.

1. Écriture re­com­man­dée :

ENV version="1.0"

2. Écriture al­ter­na­tive pour ré­tro­com­pa­ti­bi­lité :

ENV version 1.0
Conseil

L’ins­truc­tion ENV fonc­tionne à peu près de la même manière que la commande « export » sur la ligne de commande.

Ins­truc­tions WORKDIR et USER

L’ins­truc­tion WORKDIR est employée pour modifier les ré­per­toires au fil du build process, de même que lors du lancement du conteneur. Appeler WORKDIR s’applique à toutes les ins­truc­tions ul­té­rieures. Au fil du build process, les ins­truc­tions RUN, COPY et ADD sont affectées. Durant l’exécution du conteneur, ceci s’applique aux ins­truc­tions CMD et EN­TRY­POINT.

Conseil

L’ins­truc­tion WORKDIR est l’équi­valent de la commande cd sur la ligne de commande.

On a recours à l’ins­truc­tion USER pour modifier l’uti­li­sa­teur (Linux) actuel, comme la manière dont l’ins­truc­tion WORKDIR est utilisée pour modifier le ré­per­toire. Vous pouvez également choisir de définir l’ap­par­te­nance de groupe de l’uti­li­sa­teur. L’appel à USER s’applique à toutes les ins­truc­tions ul­té­rieures. Au fil du build process, les ins­truc­tions RUN sont affectées par l’ap­par­te­nance d’uti­li­sa­teur et de groupe. Durant l’exécution du conteneur, ceci s’applique aux ins­truc­tions CMD et EN­TRY­POINT.

Conseil

L’ins­truc­tion USER à la commande su sur la ligne de commande.

Ins­truc­tions COPY et ADD

On a recours à l’ins­truc­tion COPY de même qu’à ADD pour ajouter des fichiers et des ré­per­toires à l’image Docker. Chacune de ces ins­truc­tions crée une nouvelle couche, laquelle est empilée tout en haut de l’image existante. La source de l’ins­truc­tion COPY est toujours le build context. Dans l’exemple suivant, nous copions un fichier readme à partir du sous-ré­per­toire « doc » dans le build context vers le ré­per­toire « app » au plus haut niveau de l’image :

COPY ./doc/readme.md /app/
Conseil

L’ins­truc­tion COPY cor­res­pond à la commande cp sur la ligne de commande.

L’ins­truc­tion ADD se comporte de manière à peu près identique, mais elle peut récupérer des res­sources URL en-dehors du build context et dézippe des fichiers com­pres­sés. En pratique, ceci peut conduire à des effets se­con­daires inat­ten­dus. Par con­sé­quent, l’uti­li­sa­tion de l’ins­truc­tion ADD est fortement dé­con­seil­lée. Vous devriez uni­que­ment utiliser l’ins­truc­tion COPY dans la plupart des cas.

Ins­truc­tion RUN

L’ins­truc­tion RUN constitue l’une des ins­truc­tions Do­cker­file les plus courantes. Lorsque nous avons recours à l’ins­truc­tion RUN, nous ins­trui­sons à Docker d’exécuter une commande de ligne de commande au cours du build process. Les chan­ge­ments en résultant sont empilés en haut de l’image existante en tant que nouvelle couche. L’ins­truc­tion RUN peut être rédigée de deux manières :

1. Notation « Shell » : les arguments passés à RUN sont exécutés dans le shell par défaut de l’image. Des symboles spé­ci­fiques et des variables d’en­vi­ron­ne­ment sont remplacés suivant les règles du shell. Voici un exemple d’un appel qui accueille l’uti­li­sa­teur actuelle à l’aide d’un subshell "$()":

RUN echo "Hello $(whoami)"

2. Notation « Exec » : au lieu de passer une commande au shell, un fichier exé­cu­table est appelé di­rec­te­ment. Des arguments ad­di­tion­nels peuvent être passés dans le process. Voici un exemple d’un appel qui invoque l’outil de dev « npm » et lui instruit d’exécuter le script « build » :

CMD ["npm", "run", " build"]
Note

En principe, l’ins­truc­tion RUN peut être utilisée pour remplacer certaines des ins­truc­tions Docker. Par exemple, l’appel « RUN cd src » équivaut à peu près à « WORKDIR src ». Cependant, cette approche crée des Do­cker­files, lesquels de­vien­nent plus difficile à lire et à gérer à mesure que leur taille augmente. Nous vous con­seil­lons donc d’avoir recours à des ins­truc­tions spé­cia­li­sées aussi souvent que possible.

Ins­truc­tions CMD et EN­TRY­POINT

L’ins­truc­tion RUN exécute une commande au fil du build process, créant une nouvelle couche dans l’image Docker. Par contraste, les ins­truc­tions CMD et EN­TRY­POINT exécutent une commande lorsque le conteneur est lancé. Il existe une dif­fé­rence subtile entre les deux ins­truc­tions.

  • EN­TRY­POINT est utilisé pour créer un conteneur qui réalise toujours la même action lorsqu’il est lancé. Dès lors, le conteneur se comporte comme un fichier exé­cu­table.
  • CMD est utilisé pour créer un conteneur qui exécute une action définie au démarrage sans aucun pa­ra­mètres sup­plé­men­taires. Il est possible de passer fa­ci­le­ment outre l’action de pré­ré­glage à l’aide de pa­ra­mètres adaptés.

Ce que chacune de ces ins­truc­tions ont en commun est le fait qu’elle ne peuvent ap­pa­raître qu’une seule fois dans un Do­cker­file. Quoi qu’il en soit, vous pouvez combiner ces ins­truc­tions. Dans ce cas, EN­TRY­POINT définit l’action par défaut à réaliser lorsque le conteneur est lancé, tandis que CMD définit les pa­ra­mètres dont il est facile de se passer pour l’action.

Notre entrée Do­cker­file :

ENTRYPOINT ["echo", "Hello"]
CMD ["World"]

Les commandes cor­res­pon­dantes sur la ligne de commande :

# Sortie "Hello World"
docker run my_image
# Sortie "Hello Moon"
docker run my_image Moon

Ins­truc­tion EXPOSE

Les con­te­neurs Docker com­mu­ni­quent à travers le réseau. Les services exécutés dans le conteneur sont adressés via des ports spécifiés. L’ins­truc­tion EXPOSE documente les as­sig­na­tions de ports et supporte les pro­to­coles TCP et UDP. Quand un conteneur est lancé avec « docker run -P », le conteneur écoute les ports définis par EXPOSE. À titre al­ter­na­tif, il est possible d’écraser les ports sé­lec­tion­nés avec « docker run -p ».

Voici un exemple. Notre Do­cker­file contient les ins­truc­tions EXPOSE suivantes :

EXPOSE 80/tcp
EXPOSE 80/udp

Les chemins suivants sont alors dis­po­nibles pour activer les ports lorsque le conteneur est lancé :

# Le conteneur écoute le trafic TCP/UDP sur le port 80
docker run -P
# Le conteneur écoute le trafic TCP sur le port 81
docker run -p 81:81/tcp

Ins­truc­tion VOLUME

Un Do­cker­file définit une image Docker qui est cons­ti­tuée de couches empilées les unes sur les autres. Les couches sont en lecture seule de sorte que le même état est toujours garanti lorsqu’un conteneur est lancé. Nous avons besoin d’un mécanisme pour échanger des données entre le conteneur exécuté et le système hôte. L’ins­truc­tion VOLUME définit un « point de montage » à l’intérieur du conteneur.

Prenez l’extrait de Do­cker­file suivant. On crée un ré­per­toire « partagé » dans le ré­per­toire au plus haut niveau de l’image, puis on spécifie que ce ré­per­toire doit être monté dans le système hôte lorsque le conteneur est lancé :

RUN mkdir /shared
VOLUME /shared

Veuillez noter que nous ne pouvons pas spécifier l’iti­né­raire réel du système hôte à l’intérieur du Do­cker­file. Par défaut, les ré­per­toires définis par l’ins­truc­tion VOLUME sont montés sur le système hôte sous « /var/lib/docker/volumes/ ».

Comment modifie-t-on un Do­cker­file ?

Gardez à l’esprit le fait qu’un Do­cker­file est un fichier texte (brut). Il peut être modifié à l’aide des méthodes ha­bi­tuelles. Parmi elle, le recours à un éditeur de texte brut, est sans doute la plus populaire. Il peut s’agir d’un éditeur muni d’une interface uti­li­sa­teur graphique. Les options ne manquent pas à cet égard. Parmi les éditeurs les plus po­pu­laires, on retrouve VSCode, Sublime Text, Atom et Notepad++. À titre al­ter­na­tif, un grand nombre d’éditeurs sont dis­po­nibles sur la ligne de commande. Outre les éditeurs d’origine Vim et Vi, les éditeurs sim­pli­fiés Pico et Nano ont conquis un grand nombre d’uti­li­sa­teurs.

Note

Éditez toujours un fichier en texte brut avec un éditeur prévu à cet effet. N’utilisez sous aucun prétexte un trai­te­ment de texte, tel que Microsoft Word, Apple Pages, Li­breOf­fice ou Ope­nOf­fice, pour modifier un Do­cker­file.

Aller au menu principal