Ecrire un Dockerfile
Cette documentation est en cours d'écriture, donc soyez indulgent pour le moment. Je compte ajouter du contenu régulièrement pour le rendre de qualité.
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
.
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}
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-get update && apt-get 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
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 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>"]
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-get update \
&& apt-get 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"]
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>...]
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-get update && apt-get 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-get update &&\
apt-get 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. C'est outil de lint
qui va vous donner plein de conseils vous
permettant de respecter un très grand nombre 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.