Aller au contenu

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-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "main.py"]

Ici, on :

  1. part d’une image de base adaptée
  2. copie le code source
  3. installe les dépendances
  4. 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.

# Commentaire
INSTRUCTION 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:stable
ONBUILD 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:829a63ad2b1389e393e5decf5df25860347d09643c335d1dc3d91d25326d3067
FROM 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.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

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

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>"]

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"]

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: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"]

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 fichier requirements.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-alpine
RUN 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 :

*.log
node_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 builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM alpine:3.14
COPY --from=builder /app/myapp /usr/local/bin/myapp
ENTRYPOINT ["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=production
ENV 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.