Dans le chapitre précédent, nous avons découvert les concepts fondamentaux de Docker et lancé nos premiers conteneurs. Mais exécuter des images existantes ne suffit pas : en production, vous devez créer vos propres images adaptées à vos applications.
C’est exactement le rôle du Dockerfile — un fichier texte qui décrit, instruction par instruction, comment construire une image Docker reproductible. Dans ce chapitre, nous allons décortiquer chaque instruction, maîtriser les multi-stage builds et apprendre à produire des images optimisées, légères et sécurisées.
Qu’est-ce qu’un Dockerfile ?#
Un Dockerfile est un fichier texte (sans extension) placé à la racine de votre projet. Il contient une série d’instructions que le moteur Docker exécute séquentiellement pour assembler une image.
Chaque instruction crée un layer (couche) dans l’image finale. Ce système de couches est au cœur de l’efficacité de Docker : les layers sont mis en cache et réutilisés entre les builds, ce qui accélère considérablement les reconstructions.
Voici le cycle de vie typique :
| |
Créons un premier Dockerfile minimal :
| |
Construction et exécution :
| |
Le flag -t attribue un tag à l’image. Le . indique le build context — le répertoire dont Docker enverra le contenu au daemon pour le build.
Anatomie d’un Dockerfile : les instructions essentielles#
FROM — l’image de base#
Toute image Docker part d’une base. FROM est obligatoirement la première instruction (hors commentaires et directives de syntaxe).
| |
Quelques conventions :
- Utilisez toujours un tag précis (
node:22-alpineplutôt quenode:latest) pour garantir la reproductibilité. - Préférez les variantes alpine ou slim pour réduire la taille de l’image.
FROM scratchpermet de partir d’une image vide — utile pour les binaires statiques Go ou Rust.
RUN — exécuter des commandes#
RUN exécute une commande dans le conteneur pendant le build et enregistre le résultat dans un nouveau layer.
| |
Point clé : chaque RUN crée un layer. Regroupez les commandes liées avec && pour limiter le nombre de layers et réduire la taille finale (le nettoyage dans un RUN séparé ne libère pas l’espace du layer précédent).
COPY et ADD — injecter des fichiers#
COPY copie des fichiers du build context vers l’image :
| |
ADD fait la même chose, mais avec deux capacités supplémentaires :
- Extraction automatique des archives tar (
.tar,.tar.gz) - Téléchargement depuis une URL
| |
En pratique : préférez systématiquement COPY sauf si vous avez explicitement besoin de l’extraction automatique. COPY est plus lisible et son comportement est prévisible.
WORKDIR — le répertoire de travail#
WORKDIR définit le répertoire courant pour les instructions suivantes (RUN, COPY, CMD, etc.). Si le répertoire n’existe pas, Docker le crée automatiquement.
| |
Évitez les RUN cd /quelque/part && ... à répétition — WORKDIR rend le Dockerfile plus lisible et maintenable.
EXPOSE — documenter les ports#
EXPOSE documente le port sur lequel le conteneur écoute. C’est une métadonnée, pas une règle de réseau — il faut toujours mapper le port au lancement avec -p.
| |
| |
ENV et ARG — les variables#
ENV définit une variable d’environnement persistante dans l’image et les conteneurs qui en découlent :
| |
ARG définit une variable disponible uniquement pendant le build :
| |
On peut surcharger un ARG au build : docker build --build-arg APP_VERSION=2.0.0 .
CMD et ENTRYPOINT — le point d’entrée#
Ces deux instructions définissent ce qui s’exécute au démarrage du conteneur, mais avec des philosophies différentes.
CMD — la commande par défaut, substituable :
| |
L’utilisateur peut la remplacer : docker run mon-app node autre-script.js
ENTRYPOINT — le point d’entrée fixe :
| |
L’utilisateur ne peut pas la remplacer facilement (il faut --entrypoint).
Le combo classique — ENTRYPOINT pour le binaire, CMD pour les arguments par défaut :
| |
Ainsi, docker run mon-app exécute python app.py, mais docker run mon-app script.py exécute python script.py.
Forme exec vs forme shell : Utilisez toujours la forme exec (tableau JSON
["cmd", "arg"]) plutôt que la forme shell (CMD cmd arg). La forme shell lance la commande via/bin/sh -c, ce qui empêche la bonne propagation des signaux UNIX (problème courant en production).
USER — sécurité par défaut#
Par défaut, les processus tournent en root dans le conteneur. C’est une mauvaise pratique. Créez un utilisateur dédié :
| |
Placez USER après l’installation des dépendances (qui nécessite souvent root) et avant le CMD.
Multi-stage builds : la puissance de la séparation#
Le problème classique : votre image de build contient le compilateur, les dépendances de dev, les fichiers sources… tout ça se retrouve dans l’image finale, qui pèse des centaines de Mo inutilement.
Les multi-stage builds résolvent ce problème en séparant le build de l’exécution dans un seul Dockerfile.
Exemple concret : application Go#
| |
Résultat :
- L’étape builder contient Go, les sources, les dépendances — ~300 Mo
- L’image finale ne contient que le binaire statique sur une base distroless — ~8 Mo
Le mot-clé COPY --from=builder permet de copier des fichiers d’une étape vers une autre. Seule la dernière étape produit l’image finale.
Exemple : application Node.js#
| |
Trois étapes, chacune avec un rôle précis : dépendances de production, build, et image finale allégée. Les devDependencies et les sources TypeScript restent dans les stages intermédiaires.
Optimisation des images#
Comprendre le cache de layers#
Docker met en cache chaque layer. Quand une instruction change, tous les layers suivants sont invalidés. L’ordre des instructions est donc crucial.
Mauvais :
| |
Tout changement de code invalide le cache de npm ci et force une réinstallation complète.
Bon :
| |
Les dépendances ne sont réinstallées que si package.json ou package-lock.json changent. C’est le pattern le plus important à retenir.
Règle générale : placez les instructions qui changent le moins souvent en premier (dépendances système, dépendances applicatives) et celles qui changent le plus souvent en dernier (code source).
Le fichier .dockerignore#
Comme .gitignore, le .dockerignore exclut des fichiers du build context. Sans lui, Docker envoie tout le répertoire au daemon, y compris les fichiers inutiles.
| |
Avantages :
- Build plus rapide — moins de données à transférer au daemon
- Sécurité — pas de risque de copier
.envou des secrets dans l’image - Cache plus stable — les fichiers non pertinents ne cassent plus le cache
Choisir la bonne image de base#
| Image | Taille typique | Usage |
|---|---|---|
ubuntu:24.04 | ~75 Mo | Dev, debug, quand on a besoin de tout |
node:22-slim | ~60 Mo | Production Node.js sans extras |
node:22-alpine | ~45 Mo | Production, image légère |
gcr.io/distroless/base | ~20 Mo | Binaires avec dépendances libc |
gcr.io/distroless/static | ~2 Mo | Binaires statiques (Go, Rust) |
scratch | 0 Mo | Image vide, binaires statiques uniquement |
Alpine utilise musl au lieu de glibc, ce qui peut causer des incompatibilités avec certains paquets natifs. Testez votre application avant de migrer.
Distroless (maintenu par Google) ne contient ni shell, ni gestionnaire de paquets — impossible de faire un exec dans le conteneur, ce qui renforce la sécurité en production.
Réduire le nombre de layers#
Chaque RUN, COPY et ADD crée un layer. Regroupez intelligemment :
| |
Le nettoyage (rm -rf) doit être dans le même RUN que l’installation pour réellement réduire la taille de l’image.
Bonnes pratiques et anti-patterns#
✅ À faire#
- Un processus par conteneur — ne lancez pas nginx + php-fpm + cron dans le même conteneur. Utilisez Docker Compose.
- Images tagguées —
FROM python:3.12-slim, jamaisFROM python:latest. - Utilisateur non-root — toujours définir un
USERavant leCMD. - Fichier
.dockerignore— systématique, dès le premier build. - Ordre des instructions — du moins changeant au plus changeant.
- Forme exec pour CMD/ENTRYPOINT —
["cmd", "arg"]pour la gestion des signaux. - Labels pour les métadonnées — identifiez vos images :
| |
❌ Anti-patterns#
- Installer des outils de debug en production —
vim,curl,htopn’ont rien à faire dans l’image finale. Utilisez les multi-stage builds. - Stocker des secrets dans l’image — jamais de
COPY .env .ouENV DB_PASSWORD=secret. Utilisez les variables d’environnement au runtime ou Docker Secrets. - Lancer
apt-get upgrade— l’image de base est votre contrat de version. Si vous avez besoin de patchs de sécurité, mettez à jour l’image de base. - Utiliser ADD pour copier des fichiers locaux —
COPYest plus explicite et suffisant. - Ignorer le
.dockerignore— copiernode_modulesou.gitdans l’image est un problème de taille ET de sécurité. - Layer de nettoyage séparé —
RUN rm -rf /var/cachedans un layer séparé n’économise rien.
Analyser ses images#
Utilisez docker history pour comprendre la composition de vos images :
| |
Pour une analyse plus poussée, dive est un excellent outil open-source :
| |
Il affiche chaque layer, sa taille, et les fichiers ajoutés/modifiés/supprimés — indispensable pour traquer le bloat.
Exercices pratiques#
Exercice 1 — Conteneuriser une app Node.js#
Créez une application Express minimaliste et son Dockerfile optimisé :
| |
Objectifs :
- Écrire un Dockerfile avec une base
node:22-alpine - Exploiter le cache en copiant
package.jsonavant le code source - Définir un utilisateur non-root
- Créer un
.dockerignoreadapté - Construire et tester : l’image finale doit peser moins de 80 Mo
Exercice 2 — Multi-stage build Go#
Créez un serveur HTTP en Go et un Dockerfile multi-stage :
| |
Objectifs :
- Étape de build avec
golang:1.23-alpine - Image finale avec
gcr.io/distroless/static-debian12 - L’image finale doit peser moins de 10 Mo
- Comparez avec un build sans multi-stage — quelle différence de taille ?
Exercice 3 — Optimisation d’un Dockerfile existant#
Analysez ce Dockerfile et identifiez tous les problèmes :
| |
Objectifs :
- Listez au moins 6 problèmes dans ce Dockerfile
- Réécrivez-le en appliquant toutes les bonnes pratiques vues dans ce chapitre
- Ajoutez un
.dockerignoreapproprié
Indice : tag imprécis, layers éclatés, secret en dur, absence de WORKDIR, forme shell du CMD, pas d’utilisateur non-root, pas de .dockerignore,
pip installsans--no-cache-dir…
Récapitulatif#
Dans ce chapitre, nous avons couvert :
- Les instructions Dockerfile —
FROM,RUN,COPY,ADD,WORKDIR,EXPOSE,ENV,ARG,CMD,ENTRYPOINTetUSER - Les multi-stage builds — séparer build et runtime pour des images minimales
- L’optimisation — cache de layers,
.dockerignore, choix de l’image de base, réduction des layers - Les bonnes pratiques — sécurité, reproductibilité, maintenabilité
- Les anti-patterns — secrets dans l’image,
latest, layers de nettoyage séparés
Un Dockerfile bien écrit, c’est la fondation d’un pipeline CI/CD fiable. Prenez le temps de l’optimiser — vos builds seront plus rapides, vos images plus légères et vos déploiements plus sûrs.
Dans le prochain chapitre, nous aborderons Docker Compose pour orchestrer des applications multi-conteneurs et gérer les volumes, réseaux et dépendances entre services.