Docker — Dockerfile et construction d'images
Sommaire
Jusqu'ici on a utilisé des images existantes. Cette fiche porte sur la construction de ses propres images, à partir d'un Dockerfile.
1. Construire une image
mkdir mon-premier-dockerfile && cd mon-premier-dockerfile
nano Dockerfile
FROM ubuntu:22.04
CMD ["echo", "Hello, Docker!"]
docker build . -t mon_image # construire, avec le tag mon_image
docker build -t mon_image:1.0 . # avec un numéro de version
docker run mon_image # exécuter
Le point final désigne le contexte de construction : le répertoire envoyé au démon.
Tous les COPY y font référence, et rien en dehors n'est accessible. D'où l'utilité d'un fichier
.dockerignore pour exclure node_modules, .git ou les fichiers
sensibles.
2. Les instructions
| Instruction | Rôle | Moment |
|---|---|---|
FROM | Image de base — toujours la première ligne | Construction |
RUN | Exécute une commande et fige le résultat dans une couche | Construction |
COPY | Copie des fichiers du contexte vers l'image | Construction |
ADD | Comme COPY, mais décompresse les archives et accepte des URL | Construction |
WORKDIR | Définit le répertoire de travail | Construction |
ENV | Variable d'environnement, persistée dans l'image | Construction + exécution |
ARG | Variable disponible uniquement pendant la construction | Construction |
EXPOSE | Documente le port utilisé | Documentation |
USER | Utilisateur d'exécution — à utiliser pour ne pas tourner en root | Exécution |
VOLUME | Déclare un point de montage destiné à persister | Exécution |
CMD | Commande par défaut, remplaçable au docker run | Exécution |
ENTRYPOINT | Commande fixe, non remplaçable par un simple argument | Exécution |
EXPOSE ne publie rien
FROM httpd
COPY index.html /usr/local/apache2/htdocs/
EXPOSE 80
CMD ["httpd", "-D", "FOREGROUND"]
EXPOSE 80 est purement déclaratif : il documente le port attendu, rien de plus.
Pour rendre le service accessible depuis l'hôte, il faut -p 80:80 au lancement. C'est la question
piège classique.
3. Le conteneur vit tant que son processus vit
C'est la règle centrale de Docker, et la source de la moitié des incompréhensions.
Un conteneur exécute un processus principal (PID 1). Quand ce processus se termine, le conteneur s'arrête. Ce n'est pas une panne, c'est le comportement attendu.
FROM alpine
CMD ["sh", "-c", "echo 'Je vais disparaître dans 5 secondes'; sleep 5"]
Après 5 secondes, le sleep se termine, donc le processus principal aussi, donc le conteneur
s'arrête. Même chose pour un simple echo, qui s'arrête immédiatement.
| Pour qu'il reste actif | Commande |
|---|---|
| Lancer un service au premier plan | CMD ["nginx", "-g", "daemon off;"] |
| Processus de veille (démonstration) | CMD ["tail", "-f", "/dev/null"] |
| Session interactive | docker run -it mon_image bash |
Corollaire : un service ne doit jamais être lancé en mode démon dans un
conteneur. daemon off pour nginx, -D FOREGROUND pour Apache. Si le service passe en
arrière-plan, le processus principal rend la main et le conteneur meurt aussitôt.
4. ARG et ENV
FROM alpine
ENV NOM="DockerStudent"
ARG env=PROD
RUN echo "Nom : $NOM — Environnement : $env"
docker build -t demo .
docker build --build-arg env=DEV -t demo . # surcharge l'ARG
docker run demo
ARG | ENV | |
|---|---|---|
| Disponible pendant | La construction seulement | Construction et exécution |
| Présent dans l'image finale | Non | Oui |
| Surchargeable | --build-arg au build | -e au run |
| Usage | Version à installer, proxy de build | Configuration de l'application |
Pourquoi un ARG n'est-il plus disponible ensuite ? Parce qu'il n'appartient pas à l'image : c'est
un paramètre du processus de build, consommé pendant celui-ci et non enregistré dans les métadonnées.
Attention néanmoins : un ARG reste visible dans l'historique des
couches (docker history). Ne jamais y passer un mot de passe ou un jeton. Pour les secrets
de build, utiliser --secret (BuildKit).
5. ENTRYPOINT et CMD
CMD seul
FROM ubuntu:22.04
CMD ["sleep", "10"]
docker run -it mon_image # bloque 10 secondes
docker run -it mon_image bash # ouvre un shell : CMD est REMPLACÉ
On obtient un terminal au lieu du sleep parce que CMD est une valeur par
défaut, écrasée par tout argument passé au docker run.
ENTRYPOINT avec CMD
FROM ubuntu:22.04
ENTRYPOINT ["sleep"]
CMD ["10"]
docker run -it mon_image # sleep 10
docker run -it mon_image 20 # sleep 20
Ici, 20 ne remplace pas la commande mais l'argument par défaut : l'argument du
docker run se substitue au CMD, et le tout est passé à l'ENTRYPOINT, qui
reste fixe. D'où sleep 20 au lieu de sleep 10.
| Configuration | docker run img | docker run img arg |
|---|---|---|
CMD ["sleep","10"] | sleep 10 | arg (CMD écrasé) |
ENTRYPOINT ["sleep"] + CMD ["10"] | sleep 10 | sleep arg |
Règle d'usage : ENTRYPOINT définit ce que fait l'image, CMD fournit
ses arguments par défaut. Pour surcharger l'ENTRYPOINT malgré tout :
--entrypoint.
Toujours préférer la forme exec (une liste JSON) à la forme shell : cette dernière lance un
/bin/sh -c intermédiaire qui devient le PID 1 et ne transmet pas correctement les signaux — le
conteneur ne répond alors plus proprement à docker stop.
6. Les couches et le cache
Chaque instruction RUN, COPY ou ADD crée une couche.
Docker met chaque couche en cache : si rien n'a changé, il la réutilise au lieu de la reconstruire.
# MAUVAIS : toute modification du code réinstalle les dépendances
COPY . /app
RUN npm install
# BON : les dépendances ne sont réinstallées que si package.json change
COPY package.json package-lock.json ./
RUN npm install
COPY . /app
Principe d'optimisation : placer les instructions les plus stables en premier, les plus changeantes à la fin. Une couche invalidée invalide toutes les suivantes.
# regrouper les RUN limite le nombre de couches
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
Point de sécurité : les couches sont empilées, jamais effacées. Un fichier ajouté dans une
couche puis supprimé dans la suivante reste présent dans l'image et récupérable. Un secret copié par erreur ne
s'efface donc pas par un RUN rm.
7. Multi-stage build
Objectif : construire dans une image complète, puis ne garder que le résultat dans une image finale minimale.
# Étape 1 : construction
FROM node:18 AS builder
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install --only=production
# Étape 2 : image finale, allégée
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
docker build -t node-optimise .
docker run -d -p 3000:3000 node-optimise
docker images # comparer la taille avec une image mono-étape
L'intérêt est double : l'image finale ne contient ni les outils de compilation, ni les sources, ni le cache, ce qui réduit fortement sa taille et sa surface d'attaque. Un compilateur absent est un compilateur qu'un attaquant ne peut pas utiliser.
COPY --from=builder est la clé : il récupère un chemin de l'étape précédente sans embarquer le
reste. Une image Node complète pèse environ 1 Go, sa variante alpine quelques dizaines de mégaoctets.
8. Bonnes pratiques
| Règle | Pourquoi |
|---|---|
| Partir d'une image officielle et taguée | Reproductibilité et confiance |
| Préférer les variantes alpine ou slim | Taille et surface d'attaque réduites |
| Un .dockerignore | Ne pas envoyer .git, les secrets et les dépendances au démon |
| Instructions stables en premier | Cache efficace, builds rapides |
Regrouper les RUN et nettoyer dans la même couche | Moins de couches, image plus petite |
USER non privilégié | Ne pas exécuter l'application en root |
| Forme exec pour CMD et ENTRYPOINT | Signaux transmis correctement |
Jamais de secret dans un ARG ou une couche | Les couches ne s'effacent pas |
| Multi-stage dès qu'il y a une compilation | Image finale minimale |
À retenir
FROMtoujours en première ligne- Le point final du
build= contexte envoyé au démon →.dockerignore EXPOSEne publie rien — il faut-pau run- Le conteneur s'arrête quand son processus principal se termine
- Jamais de service en mode démon →
daemon off,-D FOREGROUND - ARG = build seulement | ENV = build et exécution
- CMD est écrasé par les arguments du
run - ENTRYPOINT reste fixe, CMD lui fournit ses arguments par défaut
- Préférer la forme exec (liste JSON) : signaux correctement transmis
- Dépendances avant le code source → le cache reste valide
- Les couches ne s'effacent jamais → un secret copié reste récupérable
- Multi-stage : image plus petite et surface d'attaque réduite