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.

Concepts Fondamentaux

Avant de plonger dans l’écriture d’un Dockerfile, il est indispensable de bien comprendre les concepts fondamentaux liés aux images de conteneurs. Une bonne maîtrise de ces notions vous permettra d’écrire des Dockerfiles plus efficaces et optimisés.

Si vous n’êtes pas encore familier avec les concepts de base, je vous invite à consulter le guide précédent Concepts Fondamentaux des Images de Conteneurs. Ce guide couvre en détail des éléments essentiels tels que :

  • Qu’est-ce qu’une image de conteneur ? Une vue d’ensemble sur la structure et le rôle d’une image de conteneur dans la conteneurisation.
  • Structure des images de conteneurs : Comprendre les couches qui composent une image et comment elles interagissent.
  • Choix de l’image de base : Pourquoi le choix d’une image de base est important et comment faire un choix éclairé.
  • Instructions fondamentales dans un Dockerfile : Un aperçu des commandes de base telles que FROM, RUN, COPY, CMD et comment elles fonctionnent ensemble.
  • Optimisation de la taille des images : Les meilleures pratiques pour garder vos images légères et performantes.
  • Construction et distribution des images : Les étapes finales pour construire une image localement et la partager via un registre de conteneurs.

En vous familiarisant avec ces concepts, vous serez mieux préparé à comprendre et à appliquer les bonnes pratiques lors de l’écriture de votre Dockerfile. Le guide précédent est un excellent point de départ pour consolider ces bases avant de continuer avec les aspects plus pratiques et techniques abordés dans ce guide.

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

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 productixon.

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.

Conteneur-Structure-Test

Conteneur-Structure-Test est un outil qui permet de valider la structure de vos images Docker. Il peut vérifier l’exécution des commandes, les métadonnées, et le contenu du système de fichiers pour s’assurer que votre image est conforme à vos attentes.

Toutes les informations sur conteneur-structure-test sont disponibles dans ce billet.

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.