Construire une image Docker avec un Dockerfile
Docker, conteneurs et images, c'est quoi déjà ?
Pour faire simple, Docker est un outil qui permet de gérer et d'exécuter des applications grâce à des conteneurs.
Les conteneurs, ce sont des boîtes virtuelles qui contiennent une application avec toutes ses dépendances, tout en l'isolant et en contrôlant ses interactions avec les autres conteneurs et le système d'exploitation (OS) de la machine hôte.
Enfin les images Docker contiennent les instructions et les fichiers qui permettent la création de conteneurs d'un type spécifique.
La conteneurisation offre plusieurs avantages :
-
Uniformisation des environnements : les différents environnements (développement, test, production...) peuvent utiliser les mêmes images (ou très similaires) permettant une cohérence ;
-
Amélioration de la scalabilité : la possibilité d’exécuter en parallèle la même application sur une machine en la lançant dans des conteneurs différents permet par exemple une meilleure gestion des montées en charge ;
-
Sécurité renforcée : l'isolation des conteneurs et la gestion précise de leurs flux peuvent limiter certaines attaques et vulnérabilités.
La structure du fichier
Un fichier Dockerfile se présente sous la forme d'un fichier texte contenant une suite d'instructions et de paramètres. Voici un exemple :
Fichier : Dockerfile (dockerfile)1# Exemple de fichier Dockerfile 2ARG nodeversion=23-alpine # Création d'un argument global 3 4# Définition d'une nouvelle image avec l'image de base spécifiée 5FROM node:${nodeversion} 6# Création d'une variable d'environnement 7ENV APP_NAME=App3000 8# Copie de fichiers de l'hôte vers l'image 9COPY package*.json ./ 10# Exécution d'une commande dans l'image 11RUN npm ci --only=production 12# Utilisation de l'utilisateur node pour les prochaines instructions 13USER node 14# Copie du reste des fichiers dans l'image avec le propriétaire `node` 15COPY . . 16# Indique que le port 3000 est exposé par l'application 17EXPOSE 3000 18# Spécifie le fichier à exécuter au lancement 19CMD ["node", "server.js"]
Les principales instructions
Commande | Description |
---|---|
FROM |
|
ARG |
|
ENV |
|
RUN |
|
COPY |
|
ADD |
|
EXPOSE |
|
WORKDIR |
|
ENTRYPOINT & CMD |
|
USER |
|
La liste complète des instructions est disponible sur la documentation dédiée au Dockerfile.
Fichier .dockerignore
Un fichier .dockerignore peut être créé afin d'empêcher la copie de certains éléments dans l'image finale.
Fichier : .dockerignore (dockerignore)1# Ignore les fichiers et dossiers suivants 2.dockerignore 3.gitignore 4.git 5 6*.env # Empêche la copie des fichiers .env 7?.txt # Empêche la copie des fichiers a.txt, b.txt... mais pas aa.txt 8!.git/keep # Conserve le fichier keep
Le fichier .dockerignore n'agit que sur les commandes COPY
et ADD
du répertoire local vers l'image. La copie de fichiers d'une image à une autre ne sera pas impactée.
Variante des images de base (clause FROM
)
Un bon choix de la variante de l'image de base peut améliorer le poids, les performances et réduire les vulnérabilités de votre projet.
Les informations sont généralement disponibles sur la page dédiée à l'image lorsque vous utilisez une image populaire. Voici néanmoins trois variantes populaires utilisées par Node et Nginx :
XXX:<version> : Image par défaut. Basée sur Debian et intégrant les paquets courants de la distribution, elle répond donc à la plupart des cas d'utilisation. Cela simplifie les évolutions des projets car il n'y aura pas forcément besoin de modifier le Dockerfile régulièrement lors de l'ajout d'une fonctionnalité par exemple.
XXX:<version>-alpine : Basée sur Alpine Linux, elle vise à créer des images légères. Elle n'embarque pas les paquets courants (comme git ou bash) et peut donc nécessiter des installations de bibliothèques logicielles ou des adaptations.
XXX:<version>-slim : Basée sur Debian, elle vise également à réduire la taille de l'image finale. Elle n'embarque pas les paquets courants et peut donc également nécessiter des adaptations.
Voici un comparatif des images obtenues pour un petit projet Node sans avoir cherché à adapter mon projet :
Variante | Poids | Note |
---|---|---|
node:20 | 897,64 Mio | Présence d'utilitaires dont je n'ai pas besoin. |
node:20-alpine | 563,39 Mio (-37%) | La bibliothèque npm bcrypt plantait. |
node:20-slim | 586,43 Mio (-34,5%) | - |
D'ailleurs, si vous voulez savoir s'il vaut mieux utiliser les empreintes (hash) ou les tags des images, j'en ai fait un article.
Création d'images intermédiaires
Des images intermédiaires peuvent être créées en plaçant plusieurs instructions FROM
dans le fichier. Elles peuvent permettre d'optimiser l'utilisation du cache Docker, de réaliser une image finale optimisée ou encore d'empêcher des informations sensibles (mots de passe, secrets...) d'être accessibles sur l'image finale.
Voici un exemple permettant de réduire la taille de l'image finale :
Fichier : Dockerfile (dockerfile)1# Variable globale disponible pendant le build 2ARG nodeversion=23 3 4# Étape 1 : Construction du projet 5FROM node:${nodeversion} as builder 6# Définition du répertoire de travail 7WORKDIR /usr/src/app 8# Copie des fichiers de dépendances avant l'installation 9COPY package*.json ./ 10# Installation des dépendances en mode production 11RUN npm ci --only=production 12# Copie du code source après l'installation des dépendances 13COPY . . 14# Construction du projet 15RUN npm run build --progress --display-error-details 16 17# Étape 2 : Création de l'image finale plus légère 18FROM node:${nodeversion} 19# Définition du répertoire de travail 20WORKDIR /usr/src/app 21# Copie uniquement les fichiers nécessaires depuis l'étape précédente 22COPY /usr/src/app . 23# Port utilisé par l'application 24EXPOSE 3000 25# Commande de démarrage 26CMD ["node", "server.js"]
Type | Poids |
---|---|
Sans image intermédiaire | 897,64 Mio |
Avec image intermédiaire | 730,36 Mio (-18,6%) |
Utiliser le cache Docker
La construction des images docker fonctionne grâce à un système de couches (layers). Lorsqu'une image est reconstruite, Docker peut gagner du temps et passer certaines étapes en repartant d'une couche antérieure des images précédentes
Par exemple :
Fichier : Dockerfile (dockerfile)1FROM node 2COPY package*.json ./ 3RUN npm ci 4COPY . .
pourrait être simplifié en :
Fichier : Dockerfile (dockerfile)1FROM node 2COPY . . 3RUN npm ci
mais cela ferait perdre les bénéfices du cache Docker.
En effet, dans le premier, si je reconstruis mon image et que les fichiers package*.json
n'ont pas changé, Docker reprendra la construction directement à la ligne 4 sans avoir besoin d'exécuter de nouveau la commande npm ci
.
Dans le deuxième cas, même si j'ajoute un espace dans un fichier quelconque, Docker devra tout réexécuter.
Il en est de même avec l'instruction ADD
. Si vous ajoutez par exemple un répertoire Git avant de lancer un calcul dessus. Si le dépôt Git n'a pas évolué, Docker pourrait passer le calcul.
L'ordre des différentes instructions a donc son importance dans le fichier final. Si toutefois, vous aviez plusieurs étapes consommatrices en ressources et qui pourraient être mise en cache, il est alors possible d'effectuer plusieurs images intermédiaires pour copier les résultats dans votre image finale.
Génération de l'image
En ligne de commande
L'image peut être construite avec la commande suivante :
(bash)1docker build -t <AppName> . 2 3# Pour spécifier le tag (latest par défaut), il faut écrire <AppName>:<Tag> 4docker build -t <AppName>:<Tag> . 5 6# Pour réécrire des arguments, il faut utiliser `--build-arg` : 7docker build --build-arg MON_ARG=VALUE -t <AppName> .
Depuis un docker-compose
L'image peut aussi être créée directement depuis un fichier docker-compose
:
Fichier : docker-compose.yml (yaml)1services: 2 mon-service: 3 build: 4 context: . # Dossier contenant le Dockerfile 5 dockerfile: Dockerfile # Nom du fichier (optionnel si le fichier s'appelle "Dockerfile") 6 args: 7 NODE_VERSION: "18" 8 environment: 9 NODE_ENV: "development" 10 image: mon-application:latest # Nom de l'image générée 11 ports: 12 - "3000:3000"
Il faudra alors lancer la commande suivante :
(bash)1docker-compose up --build
Documentation
En suivant ce lien, vous trouverez la documentation officielle sur le Dockerfile.
― Valentin LORTET