Aller au contenu principal

Ecrire un Dockerfile

Dans la section précédente, nous avons vu ce qu'est une image de conteneur. Nous allons voir maintenant comment les construire avec le fichier Dockerfile.

La syntaxe d'un Dockerfile

Comme dit précédemment la syntaxe d'un Dockerfile est assez simple, car elle ne fait appel qu'à une dizaine d'instructions. Chaque instruction possède des arguments.

# Commentaire
INSTRUCTION argument1 argument2

Les instructions Dockerfile

L'instruction FROM

Pour qu'un fichier Dockerfile soit valide, il doit commencer avec l'instruction FROM qui définit l'image de base. Cette image de base peut être n’importe quelle image valide.

FROM [--platform=<platform>] <image> [AS <name>]

ou

FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]

ou

FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]

L'argument facultatif --platform permet de spécifier la plate-forme de l'image dans le cas où FROM fait référence à une image multiplateforme. Par exemple, linux/amd64. Par défaut, la plateforme utilisée est celle où est effectuée la construction de l'image.

Les valeurs tag ou digest sont facultatives. Si vous omettez l'un ou l'autre, le générateur suppose que vous voulez récupérer l'image possédant le tag latest.

attention

L'utilisation d'image source possédant le tag latest est fortement déconseillé. Rien ne garantit que si vous lancez à plusieurs reprises la construction d'une image à partir d'un Dockerfile contenant une instruction FROM sans valeurs tag ou digest, vous obteniez le même résultat. Il suffit qu'entre deux exécutions cette image possédant cette balise latest ait été mise à jour pour que le résultat obtenu soit différent. La meilleure des pratiques est d'utiliser un digest.

FROM nginx:1.15.3-alpine@sha256:829a63ad2b1389e393e5decf5df25860347d09643c335d1dc3d91d25326d3067

L'instruction FROM peut apparaître plusieurs fois dans un Dockerfile. Nous verrons plus tard ce principe qui permet d'optimiser la taille des images produites avec ce qu'on appelle le multi-stage.

L'instruction LABEL

L'instruction LABEL permet d'ajouter des informations, des métadonnées, à l'image sous forme de clés / valeurs.

LABEL <key>=<value> <key>=<value> <key>=<value> ...

Les étiquettes incluses dans les images de base sont transmises aux images produites. Si une étiquette existe, la valeur appliquée sera la plus récente.

LABEL version="1.0"
LABEL description="This text illustrates \
that label-values can span multiple lines."

L'instruction ARG

L'instruction ARG est la seule instruction qui peut précéder l'instruction FROM. Elle permet de définir des variables qui peuvent être transmises au moment de la construction de l'image.

ARG <name>[=<default value>]

Il est possible de définir des valeurs par défaut avec l'opérateur = suivi de cette valeur.

ARG version=1.15.3-alpine@sha256:829a63ad2b1389e393e5decf5df25860347d09643c335d1dc3d91d25326d3067

Pour utiliser la variable, on utilise la même syntaxe qu'on utilise dans les scripts shell :

RUN echo ${VERSION}

Attention à la portée des variables ARG. En effet, un variable entre en vigueur à partir de la ligne sur laquelle elle est définie !

Les instructions FROM peuvent utiliser des variables déclarées avant elle.

ARG version=1.15.3-alpine@sha256:829a63ad2b1389e393e5decf5df25860347d09643c335d1dc3d91d25326d3067
FROM nginx:${VERSION}
attention

Il est fortement déconseillé d'utiliser des variables pour transmettre des secrets tels que des clés SSH, des mots de passe, etc. Ces valeurs sont visibles par tout utilisateur de l'image avec la commande docker history par exemple.

L'instruction ENV

Contrairement à ARG, l'instruction ENV permet de définir des variables d'environnement qui seront utilisés dans les commandes exécutées avec l'instruction RUN que nous verrons juste après. Ces variables sont persistantes et seront disponibles au moment de l'exécution de l'image de conteneur.

ENV <key>=<value> ...

Cela peut être très utile configurer des services faisant appel à des variables d'environnement. Par exemple pour une image proposant un service de Base De Données Postgres la variable d'environnement PGDATA qui indique où se trouvent les données des bases.

ENV PGDATA=/data

Elles peuvent utiliser des variables définies avec ARG.

ARG PG_VERSION=9.3.4
ENV PG_VERSION=${PG_VERSION}

Les variables d'environnement définies avec ENV seront disponibles au moment de l'exécution de l'image du conteneur. Une étape hérite de toutes les variables d'environnement définies dans l'étape parent. Donc attention cela peut provoquer des comportements inattendus.

Si une variable d'environnement n'est nécessaire que lors de la construction, et non dans l'image finale, envisagez plutôt de définir une valeur pour une seule commande :

RUN DEBIAN_FRONTEND=noninteractive apt update && apt install -y curl

ou en utilisant l'instruction ARG vu précédemment

ARG DEBIAN_FRONTEND=noninteractive

L'instruction RUN

L'instruction RUN permet de lancer des commandes aux moments de la construction de l'image.

Par exemple pour installer des packages depuis une image de base Alpine :

RUN apk update
RUN apk add nginx

Pour optimiser la taille des images, car chaque instruction RUN ajoute une couche à l'image résultante, il est conseillé d'enchainer les commandes en les séparant avec la séquence &&.

RUN apk update && apk add nginx

Lorsque beaucoup de commande s'enchainent, on peut utiliser un antislash \ pour poursuivre les commandes de l'instruction RUN sur la ligne suivante.

RUN apk update &&\
    apk add nginx
astuce

Lors de la construction de l'image, pour simplifier le debug, je vous conseille d'utiliser plusieurs instructions RUN. Une fois l'image au point, regrouper les !

L'instruction COPY

L'instruction COPY permet d'intégrer des fichiers et ou des dossiers pour les ajouter au système de fichiers de l'image dans le répertoire <dest>.

COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
#ou
COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]

La seconde forme est à utiliser si vous avez des espaces dans les noms de fichiers. Comme vous pouvez le remarquer, on peut indiquer plusieurs sources dans une seule instruction.

COPY . /app
attention

Attention les arguments optionnels --chown et --chmod ne fonctionnent que sur des images de conteneurs Linux.

L'instruction ADD

L'instruction ADD fonctionne comme l'instruction COPY sauf qu'il permet d'intégrer des URL de fichiers distants et/ou des archives qui seront décompressés à la volée et les ajoute au système de fichiers de l'image dans le répertoire <dest>.

ADD [--chown=<user>:<group>] [--chmod=<perms>] [--checksum=<checksum>] <src>... <dest>
# ou
ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
astuce

Vous l'aurez remarqué les instructions ADD et COPY sont fonctionnellement similaires. Mais il est conseillé de privilégier COPY parce qu'elle est plus transparente que ADD. Par contre si vous devez ajouter une archive ou une URL distante alors faites appel à l'instruction ADD

L'instruction USER

L'instruction USER définit l'utilisateur ou l'UID et éventuellement le groupe d'utilisateurs ou le GID à utiliser pour le reste de l'étape en cours. L'utilisateur spécifié est utilisé pour les instructions RUN et, au moment de l'exécution de l'image de conteneur.

USER <user>[:<group>]
#ou
USER UID[:GID]

Un exemple permettant de spécifier l'UID et le GID via des ARG's :

FROM alpine@sha256:d7342993700f8cd7aba8496c2d0e57be0666e80b4c441925fc6f9361fa81d10e
ARG USER_UID=1000
ARG USER_GID=${USER_UID}

RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    && apt update \
    && apt install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

USER $USERNAME

L'instruction WORKDIR

L'instruction WORKDIR définit le répertoire de travail pour toutes les instructions qui la suivent dans le Dockerfile. Si le répertoire n'existe pas, il sera créé.

WORKDIR /path/to/workdir

L'instruction VOLUME

L'instruction VOLUME crée un point de montage avec le nom spécifié et le marque comme contenant des volumes montés en externe à partir d'un hôte natif ou d'autres conteneurs.

Les volumes sont utiles pour stocker des données qui doivent persister même si le conteneur est arrêté ou supprimé.

VOLUME ["/data"]
astuce

L'instruction VOLUME permet aussi de partager des donnes entre plusieurs conteneurs.

L'instruction EXPOSE

L'instruction EXPOSE informe le moteur de conteneur que le conteneur écoute sur les ports réseau spécifiés au moment de l'exécution. Vous pouvez spécifier le protocole TCP ou UDP, TCP étant la valeur par défaut.

EXPOSE <port> [<port>/<protocol>...]
attention

L'instruction EXPOSE ne publie pas réellement le port. Il est utilisé à des fins de documentations. Il faudra le spécifier dans les arguments de la commande lançant l'image de conteneur.

Les instructions CMD et ENTRYPOINT

Ces deux instructions permettent de définir les commandes qui seront exécutées au moment du lancement de l'image de conteneur.

La principale différence entre CMD et ENTRYPOINT est que les commandes fournies par CMD peuvent être remplacés, alors que celles fournies par ENTRYPOINT ne le peuvent pas à moins de les forcer avec le bon argument dans la CLI de votre moteur de conteneur.

CMD ["executable","param1","param2"]

ENTRYPOINT ["executable", "param1", "param2"]

Exemple :

FROM debian:stable
RUN apt update && apt install -y --force-yes apache2
EXPOSE 80 443
VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]

Notre première image de conteneur

Je vous propose un exemple complet de Dockerfile faisant appel à un bon nombre de ces instructions.

FROM python:3.11-slim-bullseye AS builder
ENV PYROOT=/venv
ENV PYTHONUSERBASE=$PYROOT
WORKDIR /
COPY pyproject.toml ./
RUN apt update &&\
    apt install --no-install-recommends -y build-essential libffi-dev libssh-dev python3-dev &&\
    pip install --no-cache-dir poetry==1.2.0 setuptools==65.3.0 pip==22.2.2 --upgrade &&\
    poetry lock &&\
    poetry export -f requirements.txt --output requirements.txt &&\
    python3 -m venv venv &&\
    /venv/bin/pip install -r requirements.txt

FROM gcr.io/distroless/python3-debian11@sha256:4e7f0e61fd2bd45064a10ff3e985486b083e139461e16f004baa684eefffea8b
ENV PYTHONPATH=/venv/lib/python3.10/site-packages
COPY --from=builder /venv /venv
ENTRYPOINT ["python", "/venv/bin/lastversion"]

Cet exemple illustre le concept de ce qu'on appelle le multi-stage en utilisant deux étapes, deux instructions FROM. La première permet de construire l'image en intégrant tout ce qui est nécessaire à la construction de l'image, la seconde ne copie que le nécessaire à son exécution. Cela permet d'optimiser la taille de l'image résultante.

Parmi les points importants :

  • Les variables définies avec les instructions ENV dans la première étape sont disponibles dans la seconde.
  • On copie des données de la première étape dans la seconde en utilisant l'argument --from=<nom de l'étape>.
  • La commande qui sera lancée au moment du lancement de l'image sera celle définie par l'instruction ENTRYPOINT.

Maintenant que vous avez vu tous ces concepts et ses définitions, vous devriez être prêt pour créer des Dockerfile de qualité. Voyons à présent quelques outils qui vont vous aider dans cette tâche.

Des outils de contrôle

Je vous propose quelques outils qui vont vous permettre de progresser dans la création des Dockerfile.

Hadolint

Hadolint est l'outil qui va vous aider à progresser dans l'écriture de vos Dockerfile. Cet outil de lint va vous donner plein de conseils qui vont vous permettre de respecter un très grand nombre de bonnes pratiques. J'ai documenté son utilisation dans ce billet.

Dive

dive est l'outil d'analyse de conteneur. J'ai documenté son utilisation dans la partie parlant de l'optimisation de la taille des images de conteneurs.

conteneur-structure-test

L'outil conteneur-structure-test permet de valider la structure d'une image de conteneur. Il peut être utilisé pour vérifier le résultat des commandes d'une image, ainsi que pour vérifier ses métadonnées et le contenu du système de fichiers. Tout est expliqué dans ce billet

Conclusion

Vous êtes fin prêt pour créer des Dockerfile de grande qualité. Vous pouvez aussi passer à la suite en voyant comment optimiser la taille des images ou abordant ce qu'on appelle les moteurs de conteneurs.