Ecrire un Dockerfile
Mise à jour :
Un Dockerfile est l’élément fondamental pour automatiser la création d’images Docker. Bien conçu, il permet de garantir des déploiements rapides, fiables et sécurisés. Dans ce guide, nous explorerons les meilleures pratiques pour rédiger un Dockerfile efficace, afin que vos applications puissent être déployées de manière optimale.
Prérequis
Avant d’écrire notre premier Dockerfile, faisons un petit rappel du guide précédent. Si ce n’est pas encore fait, je te conseille de jeter un œil à ce guide Concepts Fondamentaux des Images de Conteneurs. Tu y découvriras tout ce qu’il faut savoir sur :
- la structure des images
- les couches empilées
- et le choix de l’image de base
Ces bases sont essentielles pour comprendre ce que l’on fait dans un Dockerfile.
On met quoi dans un Dockerfile ?
Un Dockerfile, c’est tout simplement une suite d’instructions qui permettent de créer une image fonctionnelle à partir d’une image de base.
Il contient tout ce qu’il faut pour que ton application tourne correctement :
- installation des dépendances
- copie du code
- configuration de l’environnement
- commande de lancement de l’application
Tout dépend de ton image de base
Si tu pars d’une image très complète comme python:3.11-slim
, tu auras
peut-être juste à copier ton code et installer les requirements.
Mais si tu pars d’une image très minimale comme alpine
, tu devras probablement
:
- installer des packages système
- configurer manuellement certains outils
- ajouter des scripts ou fichiers supplémentaires
Plus l’image de base est simple, plus tu auras de commandes à écrire dans le Dockerfile pour rendre ton environnement prêt à l’emploi.
Exemple simple :
FROM python:3.11-slimWORKDIR /appCOPY . .RUN pip install -r requirements.txtCMD ["python", "main.py"]
Ici, on :
- part d’une image de base adaptée
- copie le code source
- installe les dépendances
- et lance l’application
Et c’est tout ! Pas besoin de plus pour une app Python classique.
Et maintenant ?
Maintenant qu’on sait ce qu’on met dans un Dockerfile, il est temps de passer à la description détaillée des principales instructions que l’on va utiliser.
👉 On commence avec les plus classiques : FROM
, RUN
, COPY
, CMD
, ENV
,
WORKDIR
, etc.
La syntaxe d’un Dockerfile
Un Dockerfile est un fichier texte qui décrit, étape par étape, comment construire une image de conteneur. Chaque ligne du Dockerfile correspond à une instruction qui précise une action à exécuter pour configurer l’image. Ces instructions suivent une syntaxe simple : Instruction suivie d’un ou plusieurs arguments, qui définissent les détails de l’action.
# CommentaireINSTRUCTION argument1 argument2
Par exemple, l’instruction FROM
indique l’image de base sur laquelle sera
construite l’image finale, tandis que COPY
permet de copier des fichiers
depuis votre machine locale vers l’image. L’ordre dans lequel ces instructions
sont placées est important, car il détermine la séquence des étapes de
construction et la structure finale de l’image.
Un Dockerfile typique peut contenir des instructions comme RUN
pour
exécuter des commandes dans le conteneur, ENV
pour définir des variables
d’environnement, ou encore CMD
pour spécifier la commande à exécuter lorsque
le conteneur démarre. Ces instructions sont essentielles pour personnaliser et
optimiser l’image de conteneur en fonction des besoins spécifiques de votre
application.
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’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 ONBUILD
L’instruction ONBUILD
permet de définir une instruction qui sera exécutée
lorsque l’image sera utilisée comme image de base pour une autre image. Cela
peut être utile pour créer des images de base qui incluent des instructions
supplémentaires à exécuter lors de la construction d’une image dérivée.
ONBUILD <INSTRUCTION>
Exemple :
FROM debian:stableONBUILD RUN apt-get update && apt-get install -y python3
Dans cet exemple, lorsque l’image de base debian:stable
est utilisée pour
créer une nouvelle image, la commande RUN
sera exécutée automatiquement,
installant python3 dans l’image dérivée.
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, une 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:829a63ad2b1389e393e5decf5df25860347d09643c335d1dc3d91d25326d3067FROM nginx:${VERSION}
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.4ENV 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 updateRUN 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
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>#ouCOPY [--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
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># ouADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
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>]#ouUSER UID[:GID]
Un exemple permettant de spécifier l’UID et le GID via des ARG’s :
FROM alpine@sha256:d7342993700f8cd7aba8496c2d0e57be0666e80b4c441925fc6f9361fa81d10eARG USER_UID=1000ARG 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"]
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>...]
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:stableRUN apt update && apt install -y --force-yes apache2EXPOSE 80 443VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"]ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
L’instruction HEALTHCHECK
L’instruction HEALTHCHECK
permet de définir une commande qui sera exécutée
périodiquement pour vérifier si le conteneur est en bonne santé. Si la
commande échoue, le conteneur sera marqué comme “non sain”. Cela peut être
utile pour surveiller l’état de l’application à l’intérieur du conteneur et
prendre des mesures si nécessaire.
HEALTHCHECK [OPTIONS] CMD command
ou
HEALTHCHECK [OPTIONS] NONE
Exemple :
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \ CMD curl -f http://localhost/ || exit 1
L’instruction STOPSIGNAL
L’instruction STOPSIGNAL
permet de définir le signal qui sera envoyé au
conteneur lorsqu’il sera arrêté. Cela peut être utile pour personnaliser le
comportement d’arrêt du conteneur en fonction de l’application qu’il exécute.
STOPSIGNAL <signal>
Exemple :
STOPSIGNAL SIGKILL
L’instruction SHELL
L’instruction SHELL
permet de définir le shell à utiliser pour exécuter les
commandes dans le conteneur. Par défaut, Docker utilise /bin/sh -c
, mais
vous pouvez le remplacer par un autre shell si nécessaire.
SHELL ["executable", "parameters"]
ou
SHELL ["executable", "param1", "param2"]
Exemple :
SHELL ["/bin/bash", "-c"]
Construire une première image
Dans ce chapitre, nous allons voir comment construire une image Docker basique pour une application Python. Ce guide est parfait pour ceux qui débutent avec Docker et souhaitent comprendre les concepts fondamentaux en construisant une image simple et fonctionnelle. Nous allons utiliser un Dockerfile minimaliste pour créer une image capable de lancer une application Python.
Choix de l’Image de Base
Le point de départ de tout Dockerfile est l’image de base. Pour une application Python, nous allons utiliser une image officielle de Python disponible sur Docker Hub.
FROM python:3.11-slim
Ici, nous utilisons python:3.11-slim
, une image allégée de Python basée
sur la version 3.11. L’utilisation de l’option “slim” permet de réduire la
taille de l’image, ce qui est idéal pour des déploiements rapides et efficaces.
Configuration du Répertoire de Travail
Ensuite, nous allons définir un répertoire de travail dans lequel toutes les commandes suivantes s’exécuteront. Cela simplifie la gestion des fichiers dans l’image.
WORKDIR /app
Cette instruction WORKDIR /app
crée un répertoire /app
et positionne le
conteneur à cet emplacement. Toutes les opérations suivantes, comme la copie de
fichiers ou l’installation de dépendances, se feront dans ce répertoire.
Copie du Code Source
Nous devons maintenant copier notre code source et notre fichier de dépendances
dans l’image. Supposons que votre application Python a un fichier
requirements.txt
qui liste les dépendances nécessaires.
COPY requirements.txt .COPY . .
COPY requirements.txt .
: Copie le fichierrequirements.txt
depuis votre machine locale vers le répertoire de travail du conteneur.COPY . .
: Copie l’intégralité du code source de votre projet dans le répertoire de travail du conteneur.
Installation des Dépendances
Une fois le code source copié, nous devons installer les dépendances nécessaires
à l’exécution de l’application. Cela se fait en utilisant pip
, le gestionnaire
de paquets Python.
RUN pip install --no-cache-dir -r requirements.txt
L’instruction RUN pip install --no-cache-dir -r requirements.txt
installe
toutes les dépendances listées dans requirements.txt
. L’option
--no-cache-dir
évite de stocker en cache les paquets téléchargés, ce qui
permet de garder l’image plus légère.
Définition du Point d’Entrée
Enfin, nous devons définir la commande qui sera exécutée lorsque le conteneur
démarrera. Supposons que notre application Python démarre avec un fichier appelé
app.py
.
CMD ["python", "app.py"]
L’instruction CMD ["python", "app.py"]
spécifie que, par défaut, le
conteneur exécutera python app.py
lorsque vous le lancerez. Cette commande
lancera votre application Python.
Bonnes Pratiques pour Écrire un Dockerfile de Qualité
Pour écrire des Dockerfiles performants, maintenables et sécurisés, il est essentiel de suivre certaines bonnes pratiques. Ces pratiques vous aideront à optimiser vos images, réduire leur taille et minimiser les erreurs. En complément de ces bonnes pratiques, l’utilisation d’outils de contrôle vous permettra de perfectionner votre maîtrise de la création d’images Docker. Voici quelques conseils et outils incontournables pour progresser.
Écrire des Dockerfiles Simples et Lisibles
La simplicité est clé pour écrire des Dockerfiles maintenables. Utilisez des instructions claires et évitez les combinaisons complexes de commandes sur une seule ligne. Chaque commande doit être explicite et facile à comprendre pour quiconque lit le Dockerfile.
Exemple :
RUN apt-get update && apt-get install -y \ curl \ vim \ && apt-get clean \ && rm -rf /var/lib/apt/lists/*
Cette commande est claire, installe les paquets nécessaires et nettoie ensuite pour réduire la taille de l’image.
Minimiser la Taille de l’Image
Utiliser des images de base légères, comme alpine
et combiner les commandes
pour réduire le nombre de couches sont des stratégies essentielles pour
minimiser la taille de l’image. Moins l’image est volumineuse, plus elle est
rapide à déployer et à transférer.
Exemple :
FROM python:3.11-alpineRUN pip install --no-cache-dir -r requirements.txt
En utilisant alpine
, une version allégée de Linux, vous réduisez
considérablement la taille de l’image finale.
Utiliser .dockerignore
Comme pour .gitignore
, un fichier .dockerignore
permet d’exclure les
fichiers inutiles lors de la construction de l’image, ce qui contribue à réduire
sa taille et à éviter l’inclusion de données sensibles.
Exemple :
*.lognode_modules.env
Ces fichiers ne seront pas inclus dans l’image Docker, ce qui la rendra plus légère et plus sécurisée.
Séparer les Phases de Construction et d’Exécution
Utiliser une approche multi-étape pour séparer la phase de construction (qui inclut les outils et dépendances de développement) de la phase d’exécution permet de réduire la taille de l’image finale tout en améliorant la sécurité.
Exemple :
FROM golang:1.18 AS builderWORKDIR /appCOPY . .RUN go build -o myapp
FROM alpine:3.14COPY --from=builder /app/myapp /usr/local/bin/myappENTRYPOINT ["myapp"]
Dans cet exemple, la compilation est effectuée dans une première étape, puis le binaire compilé est copié dans une image alpine légère pour l’exécution.
Gérer les Variables d’Environnement avec Précaution
Définissez les variables d’environnement (ENV
) et les arguments de
construction (ARG
) pour gérer les configurations, mais évitez d’y inclure des
informations sensibles comme des clés API ou des mots de passe.
Exemple :
ARG APP_ENV=productionENV APP_ENV=${APP_ENV}
Cette configuration permet de gérer facilement les environnements de développement et de production.
Utiliser des Outils de Contrôle
Pour garantir la qualité et l’efficacité de vos Dockerfiles, il est important d’utiliser des outils de contrôle qui vous aideront à respecter les bonnes pratiques et à optimiser vos images.
Hadolint
Hadolint est un outil de linting qui analyse vos Dockerfiles et vous donne des recommandations pour améliorer leur qualité. Il vérifie si vous suivez les bonnes pratiques et vous alerte sur les erreurs courantes.
Vous pouvez découvrir comment utiliser Hadolint dans ce billet.
Dive
dive ↗ est un outil d’analyse qui vous permet de plonger dans les couches de votre image Docker pour comprendre comment elle est construite. Il vous aide à identifier les couches inutiles et à optimiser la taille de votre image.
Pour en savoir plus sur l’utilisation de dive, consultez la section dédiée à l’optimisation de la taille des images de conteneurs.
Dockle
Dockle est un outil d’analyse de sécurité pour les images Docker. Il vérifie les meilleures pratiques de sécurité et vous alerte sur les mauvaises pratiques que vous pourriez avoir dans vos images. Il est particulièrement utile pour s’assurer que vos images sont sécurisées et conformes aux normes de sécurité des benchmarks CIS Docker.
Pour en savoir plus sur l’utilisation de Dockle, consultez le guide dédiée.
Il est temps de passer à l’action !
Maintenant que tu as compris ce qu’est un Dockerfile et ce qu’on y met, il est temps de mettre tout ça en pratique 💪
Je t’invite à suivre mon TP disponible dans mon dépôt GitHub sur la formation à la conteneurisation ↗ :
- installer tout ce qu’il faut pour te lancer
- créer ton premier Dockerfile
- construire une image personnalisée
- comprendre le rôle de chaque instruction
- tester ton image en local
- explorer les couches avec dive
Pas besoin d’être expert pour te lancer. L’objectif est de comprendre par la pratique, étape par étape. Et pas de panique, tout est guidé
Allez, on sort les claviers et on construit nos premières images !
Conclusion
En suivant ces bonnes pratiques et en utilisant les outils appropriés, vous êtes désormais prêt à créer des Dockerfiles de haute qualité. Ces outils et techniques vous aideront à progresser et à garantir que vos images Docker restent performantes, sécurisées et faciles à gérer.
Pour aller plus loin, vous pouvez explorer comment optimiser la taille des images ou découvrir les différents moteurs de conteneurs.