Dockerfile : ce qui se cache derrière ce format

Le logiciel open-source Docker s’est imposé comme la norme dans la virtualisation basées sur les conteneurs. La virtualisation basée sur les conteneurs constitue la prochaine étape dans l’évolution des machines virtuelles, mais avec une différence significative. Au lieu de simuler un système d’exploitation complet, une application unique est virtualisée dans un conteneur. De nos jours, les conteneurs Docker sont utilisés dans toutes les phases du cycle de vie du software, tels que le développement, les tests et l’exploitation.

Il existe différents concepts dans l’écosystème Docker. La connaissance et la compréhension de ces composants est essentielle pour travailler efficacement avec Docker. En particulier, on retrouve parmi ces derniers les images Docker, les conteneurs Docker, et les Dockerfiles. Nous vous expliquons le fonctionnement et donnons des conseils pratiques d’utilisation.

Qu’est-ce qu’un Dockerfile ?

Un Dockerfile constitue l’unité de base au sein de l’écosystème Docker. Il décrit les étapes de la création d’une image Docker. Le flot d’informations suit ce modèle central : Dockerfile > image Docker > conteneur Docker.

Un conteneur Docker dispose d’une durée de vie limitée et interagit avec son environnement. Imaginez que le conteneur est un organisme unicellulaire, une cellules de levure, par exemple. Suivant cette analogie, une image Docker correspond peu ou prou aux informations génétiques. Tous les conteneurs créés à partir d’une image unique sont identiques, de même que tous les organismes unicellulaires sont clonés à partir des mêmes informations génétiques. Dès lors, quel rôle jouent les Dockerfiles dans ce modèle ?

Un Dockerfile définit les étapes pour créer une nouvelle image. Il est important que vous compreniez que tout commence par une image de base existante. L’image nouvellement créée succède à l’image de base. Il y a également un certain nombre de changements spécifiques. Si nous revenons à notre exemple de cellule de levure, les changements correspondent à des mutations. Un Dockerfile 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éalogique de l’écosystème Docker.
  2. Un certain nombre de changements spécifiques qui distinguent la nouvelle image de l’image de base.

Comment fonctionne un Dockerfile et comment créer une image à partir de celui-ci ?

Un Dockerfile n’est ni plus ni moins qu’un fichier texte normal. Le Dockerfile contient un ensemble d’instructions, chacune présente sur une ligne séparée. Les instructions 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 traitement par lots. Au cours de l’exécution, des couches supplémentaires sont ajoutées à l’image étape par étape. Nous vous expliquons exactement comment cela fonctionne dans notre article consacré aux images Docker.

Une image Docker est créée en exécutant les instructions présentes dans un Dockerfile. Cette étape s’intitule le build process (« processus de développement » en français) et est enclenchée en exécutant la commande « docker build ». Le « build context » (« contexte de développement » en français) est un concept central. Ce dernier définit à quels fichiers et répertoires le build process a accès. Ici, un répertoire local fait office de source. Le sommaire du répertoire source est passé au Docker Daemon lorsque la commande « docker build » est appelée. Les instructions dans le Dockerfile accèdent aux fichiers et répertoires 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 .dockerignore à cette fin. Ce dernier est utilisé pour exclure des fichiers et répertoires 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 Dockerfile est-il structuré ?

Un Dockerfile constitue un fichier en texte brut intitulé « Dockerfile ». 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 Dockerfile :

# Commentaire
INSTRUCTION arguments

Outre les commentaires, les Dockerfiles contiennent des instructions et des arguments. Ces derniers décrivent la structure de l’image.

Commentaires et directives parser

Les commentaires contiennent des informations destinées d’abord à des humains. Par exemple, les commentaires présents au sein d’un Dockerfile commencent avec un signe dièse (#) dans Python, Perl et Ruby. Les lignes de commentaires sont supprimées durant le build process avant d’aller plus loin dans le process. Veuillez noter que seules les lignes qui commencent avec un signe dièse sont reconnues comme des lignes de commentaire.

Voici un commentaire 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 directives Parser constituent un type spécifique de commentaire. Elles sont situées dans les lignes de commentaire et doivent se situer au début du Dockerfile. Autrement, elles seront traitées comme des commentaires et supprimées durant le développement. Il est également important de noter qu’une directive parser donnée peut uniquement être utilisée une fois dans un Dockerfile.

Au moment de l’écriture de cet article, il existe seulement deux types de directives parser : « syntax » et « escape ». La directive parser « escape » définit le symbole espace à utiliser. Ceci est utilisé pour écrire des instructions sur plusieurs lignes, de même que pour exprimer des caractères spécifiques. La directive parser « syntax » spécifie les règles auxquelles le parser doit avoir recours pour procéder les instructions du Dockerfile. Voici un exemple :

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

Instructions, arguments et variables

Les instructions constituent la majorité du contenu du Dockerfile. Les instructions décrivent la structure spécifique 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 instructions mobilisent des arguments. Certaines instructions sont directement comparables à des commandes de ligne de commandes spécifiques. Dès lors, il existe une instruction COPY qui copie les fichiers et répertoires et équivaut peu ou prou à la commande cp sur la ligne de commande. Quoi qu’il en soit, une différence avec la ligne de commande est que certaines instructions Dockerfile ont des règles spécifiques pour leur séquence. Par ailleurs, certaines instructions peuvent uniquement apparaître une fois dans un Dockerfile.

Note

Les instructions n’ont pas à être obligatoirement mise en capitales. Il est quand même conseiller de suivre la convention lorsque vous créez un Dockerfile.

En ce qui concerne les arguments, vous devez établir une distinction entre les parties codées en dur et variables. Docker suit la méthodologie 12 facteurs et utilise des variables d’environnement pour configurer des conteneurs. L’instruction ENV est utilisée pour définir les variables d’environnement dans un Dockerfile. À présent, jetons un œil à la manière dont on assigne une valeur à la variable d’environnement.

Les valeurs stockées dans les variables d’environnement peuvent être lues et utilisées en tant que parties variables des arguments. Une syntaxe spécifique est utilisée à cette fin. Cela évoque les scripts shell. Le nom de la variable d’environnement est précédé par un signe dollar : $env_var. Il existe également une notation alternative pour délimiter explicitement 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 instruction Dockerfile les plus importantes

Nous allons maintenant vous présenter les instructions Dockerfile les plus importantes. Traditionnellement, certaines instructions, notamment FROM, n’étaient autorisées à apparaître qu’une seule fois dans Dockerfile. Quoi qu’il en soit, il existe maintenant des multi-stage builds. Ils décrivent plusieurs images dans un Dockerfile. La restriction s’applique alors à chaque stade de build individuel.

Instruction Description Commentaire
FROM Définir l’image de base Doit apparaître comme la première instruction ; une seule entrée par stade de build
ENV Définit les variables d’environnement pour le build process et le container runtime
ARG Déclare les paramètres de ligne de commande pour le build process Peut apparaître avant l’instruction FROM
WORKDIR Modifie le répertoire actuel
USER Modifie le statut de membre de l’utilisateur et de groupe
COPY Copie les fichiers et répertoires sur l’image Crée une nouvelle couche
ADD Copie les fichiers et répertoires sur l’image Crée une nouvelle couche ; utilisation déconseillé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
ENTRYPOINT Définit la commande par défaut pour le lancement du conteneur Une seule entrée par stade de build
EXPOSE Définit les assignations de port pour le conteneur exécuté Les ports doivent être exposés lors du lancement du conteneur
VOLUME Inclut le répertoire dans l’image en tant que volume lors du lancement du conteneur dans le système hôte

Instruction FROM

L’instruction FROM définit l’image de base sur laquelle les instructions ultérieures opèrent. Cette instruction ne peut exister qu’une fois par stade de build et doit apparaître dans la première instruction. Attention : l’instruction ARG peut apparaître avant l’instruction FROM. Vous pouvez ainsi spécifier exactement 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 exactement 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’ingrédients de base. Ce terme est utilisé en boulangerie et en cuisine. Si un fichier Docker commence par la ligne « FROM scratch », cela signifie que l’image est assemblée à partir de rien.

Instructions ENV et ARG

Ces deux instructions assignent une valeur à une variable. La distinction entre les deux instructions réside principalement dans l’origine des valeurs et le contexte dans lequel les variables sont disponibles. Commençons par jeter un œil à l’instruction ARG.

L’instruction ARG déclare une variable dans le Dockerfile qui est uniquement disponible 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 convenable 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éfinissons ici une variable d’environnement à l’aide de l’instruction ENV. Contrairement à l’instruction ARG, une variable définie avec ARG existe à la fois lors du build process et au cours de l’exécution du conteneur. L’instruction ENV peut être rédigée de deux manières.

1. Écriture recommandée :

ENV version="1.0"

2. Écriture alternative pour rétrocompatibilité :

ENV version 1.0
Conseil

L’instruction ENV fonctionne à peu près de la même manière que la commande « export » sur la ligne de commande.

Instructions WORKDIR et USER

L’instruction WORKDIR est employée pour modifier les répertoires au fil du build process, de même que lors du lancement du conteneur. Appeler WORKDIR s’applique à toutes les instructions ultérieures. Au fil du build process, les instructions RUN, COPY et ADD sont affectées. Durant l’exécution du conteneur, ceci s’applique aux instructions CMD et ENTRYPOINT.

Conseil

L’instruction WORKDIR est l’équivalent de la commande cd sur la ligne de commande.

On a recours à l’instruction USER pour modifier l’utilisateur (Linux) actuel, comme la manière dont l’instruction WORKDIR est utilisée pour modifier le répertoire. Vous pouvez également choisir de définir l’appartenance de groupe de l’utilisateur. L’appel à USER s’applique à toutes les instructions ultérieures. Au fil du build process, les instructions RUN sont affectées par l’appartenance d’utilisateur et de groupe. Durant l’exécution du conteneur, ceci s’applique aux instructions CMD et ENTRYPOINT.

Conseil

L’instruction USER à la commande su sur la ligne de commande.

Instructions COPY et ADD

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

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

L’instruction COPY correspond à la commande cp sur la ligne de commande.

L’instruction ADD se comporte de manière à peu près identique, mais elle peut récupérer des ressources URL en-dehors du build context et dézippe des fichiers compressés. En pratique, ceci peut conduire à des effets secondaires inattendus. Par conséquent, l’utilisation de l’instruction ADD est fortement déconseillée. Vous devriez uniquement utiliser l’instruction COPY dans la plupart des cas.

Instruction RUN

L’instruction RUN constitue l’une des instructions Dockerfile les plus courantes. Lorsque nous avons recours à l’instruction RUN, nous instruisons à Docker d’exécuter une commande de ligne de commande au cours du build process. Les changements en résultant sont empilés en haut de l’image existante en tant que nouvelle couche. L’instruction 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écifiques et des variables d’environnement sont remplacés suivant les règles du shell. Voici un exemple d’un appel qui accueille l’utilisateur actuelle à l’aide d’un subshell "$()":

RUN echo "Hello $(whoami)"

2. Notation « Exec » : au lieu de passer une commande au shell, un fichier exécutable est appelé directement. Des arguments additionnels 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’instruction RUN peut être utilisée pour remplacer certaines des instructions Docker. Par exemple, l’appel « RUN cd src » équivaut à peu près à « WORKDIR src ». Cependant, cette approche crée des Dockerfiles, lesquels deviennent plus difficile à lire et à gérer à mesure que leur taille augmente. Nous vous conseillons donc d’avoir recours à des instructions spécialisées aussi souvent que possible.

Instructions CMD et ENTRYPOINT

L’instruction RUN exécute une commande au fil du build process, créant une nouvelle couche dans l’image Docker. Par contraste, les instructions CMD et ENTRYPOINT exécutent une commande lorsque le conteneur est lancé. Il existe une différence subtile entre les deux instructions.

  • ENTRYPOINT 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écutable.
  • CMD est utilisé pour créer un conteneur qui exécute une action définie au démarrage sans aucun paramètres supplémentaires. Il est possible de passer facilement outre l’action de préréglage à l’aide de paramètres adaptés.

Ce que chacune de ces instructions ont en commun est le fait qu’elle ne peuvent apparaître qu’une seule fois dans un Dockerfile. Quoi qu’il en soit, vous pouvez combiner ces instructions. Dans ce cas, ENTRYPOINT définit l’action par défaut à réaliser lorsque le conteneur est lancé, tandis que CMD définit les paramètres dont il est facile de se passer pour l’action.

Notre entrée Dockerfile :

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

Les commandes correspondantes sur la ligne de commande :

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

Instruction EXPOSE

Les conteneurs Docker communiquent à travers le réseau. Les services exécutés dans le conteneur sont adressés via des ports spécifiés. L’instruction EXPOSE documente les assignations de ports et supporte les protocoles TCP et UDP. Quand un conteneur est lancé avec « docker run -P », le conteneur écoute les ports définis par EXPOSE. À titre alternatif, il est possible d’écraser les ports sélectionnés avec « docker run -p ».

Voici un exemple. Notre Dockerfile contient les instructions EXPOSE suivantes :

EXPOSE 80/tcp
EXPOSE 80/udp

Les chemins suivants sont alors disponibles 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

Instruction VOLUME

Un Dockerfile définit une image Docker qui est constitué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’instruction VOLUME définit un « point de montage » à l’intérieur du conteneur.

Prenez l’extrait de Dockerfile suivant. On crée un répertoire « partagé » dans le répertoire au plus haut niveau de l’image, puis on spécifie que ce répertoire 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’itinéraire réel du système hôte à l’intérieur du Dockerfile. Par défaut, les répertoires définis par l’instruction VOLUME sont montés sur le système hôte sous « /var/lib/docker/volumes/ ».

Comment modifie-t-on un Dockerfile ?

Gardez à l’esprit le fait qu’un Dockerfile est un fichier texte (brut). Il peut être modifié à l’aide des méthodes habituelles. 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 utilisateur graphique. Les options ne manquent pas à cet égard. Parmi les éditeurs les plus populaires, on retrouve VSCode, Sublime Text, Atom et Notepad++. À titre alternatif, un grand nombre d’éditeurs sont disponibles sur la ligne de commande. Outre les éditeurs d’origine Vim et Vi, les éditeurs simplifiés Pico et Nano ont conquis un grand nombre d’utilisateurs.

Note

Éditez toujours un fichier en texte brut avec un éditeur prévu à cet effet. N’utilisez sous aucun prétexte un traitement de texte, tel que Microsoft Word, Apple Pages, LibreOffice ou OpenOffice, pour modifier un Dockerfile.