Tutoriel Dockerfile : construire votre première image Docker (avec exemples)

Les messages d’erreur défilent dans le terminal — « COPY ../config.json: no such file or directory ». Huitième échec de build ce soir. L’app tourne en local, mais dès qu’on la met dans une image Docker, tout casse. Stack Overflow, la réponse la plus votée… l’image passe de 200 Mo à 2 Go.
Le Dockerfile du projet : FROM, RUN, COPY, CMD — chaque mot est connu, ensemble c’est flou. On copie un tutoriel, neuf fois sur dix ça ne démarre pas. Les messages restent vagues : mauvais chemin ou mauvaise instruction ?
Un Dockerfile n’est pas sorcier. Une fois le rôle de chaque instruction et les pièges courants compris, l’essentiel tient en quelques points. Cet article part de zéro pour construire votre première image : code réel à chaque instruction, plus les 3 erreurs les plus fréquentes des débutants. À la fin, vous pourrez dockeriser un projet Node.js ou Python.
Qu’est-ce qu’un Dockerfile ?
En bref, un Dockerfile est un fichier texte qui décrit toutes les étapes pour construire une image Docker. Imaginez un plan de rénovation : pose du sol, peinture, puis luminaires. Le moteur Docker suit ce plan pour « aménager » votre application et la figer en image.
Cette image est un instantané d’environnement exécutable. Votre app Node.js a besoin de Node 18, de paquets npm et de votre code ? Le Dockerfile empaquette tout. Quelqu’un lance docker run sur l’image — pas besoin de configurer l’environnement à la main.
En pratique, trois étapes :
- Choisir une base (ex. Node.js 18)
- Y installer votre code et vos dépendances
- Indiquer la commande au démarrage du conteneur
Ensuite, docker build génère l’image. Simple en surface — le diable est dans les détails. Voyons les instructions clés.
Les 6 instructions essentielles
Construire une image Docker de zéro
Écrire un Dockerfile de l’image de base au premier build et run
Estimated time: PT20M
-
1
Step 1: Étape 1 : Choisir l’image de base (FROM)
FROM doit être la première instruction — choisir la bonne base : -
2
Step 2: • Node.js
node:18-alpine (~5 Mo) -
3
Step 3: • Python
python:3.11-slim -
4
Step 4: • Nginx
nginx:alpine -
5
Step 5: Étape 2 : Répertoire de travail (WORKDIR)
WORKDIR /app -
6
Step 6: Étape 3 : Dépendances (COPY+RUN)
Copier d’abord package*.json, puis npm install -
7
Step 7: Mauvais ordre
tout copier puis installer — chaque changement de code relance npm install. -
8
Step 8: Étape 4 : Code applicatif (COPY)
Copier les sources -
9
Step 9: Étape 5 : Port (EXPOSE)
EXPOSE 3000 — documentation uniquement -
10
Step 10: • Le mapping réel
docker run -p -
11
Step 11: Étape 6 : Démarrage (CMD)
CMD [“npm”, “start”] — remplaçable par docker run -
12
Step 12: • CMD seul
service avec démarrages variables -
13
Step 13: • ENTRYPOINT+CMD
outil, commande fixe et args variables -
14
Step 14: • ENTRYPOINT seul
scénario très figé -
15
Step 15: Étape 7 : Build et run
Build : docker build -t my-app:1.0 . -
16
Step 16: Run
docker run -p 3000:3000 my-app:1.0 -
17
Step 17: Vérifier
http://localhost:3000
FROM — choisir l’image de base
FROM est la première instruction (hors commentaires et ARG). Elle définit votre « fondation ».
App Node.js ? node:18-alpine. Python ? python:3.11-slim. Reverse proxy Nginx ? nginx:alpine.
# Image de base Node.js 18 sur Alpine Linux
FROM node:18-alpine
alpine vs slim : Alpine (~5 Mo) vs image node complète (~900 Mo) — facteur ~180. Piège : musl libc au lieu de glibc ; en cas d’erreur de build native, essayer node:18-slim.
Erreur fréquente : image node au hasard, version incompatible avec package.json — aligner la version Node du FROM sur celle du projet.
RUN — commandes pendant la construction
RUN exécute des commandes à la construction (paquets, dossiers, config). Chaque RUN ajoute une couche.
Mauvais exemple :
# ❌ 3 couches
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get clean
Chaque RUN empile une couche ; même après suppression, les données des couches précédentes restent. Sept RUN dispersés m’ont donné une image de 2 Go et un upload interminable.
Bonne pratique — enchaîner avec && :
# ✅ 1 seule couche
RUN apt-get update && \
apt-get install -y python3 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Le \ est un saut de ligne. Le rm final libère des dizaines de Mo.
Ne jamais isoler RUN apt-get update : le cache de couches peut servir un index obsolète — toujours regrouper update et install.
COPY vs ADD — copier des fichiers
Les deux copient des fichiers ; COPY est direct, ADD fait plus (décompression tar, URL) — la doc Docker recommande COPY quand c’est possible.
COPY package.json /app/
COPY ./src /app/src
COPY . /app
Piège fatal : les chemins sont relatifs au contexte de build, pas au Dockerfile.
Le contexte = le répertoire du . dans docker build .. COPY n’accède qu’à ce répertoire et ses enfants.
# ❌ Hors contexte
COPY ../config.json /app/
COPY /opt/myfile.txt /app/
Sécurité et reproductibilité : pas d’accès arbitraire à l’hôte.
Solutions :
- Déplacer config.json dans le projet
- Ou
docker build -f myproject/Dockerfile .depuis le parent
ADD décompresse les archives et peut télécharger des URL — comportement moins lisible. Pour décompresser : RUN tar -xzf ; pour télécharger : RUN curl.
WORKDIR — répertoire de travail
Équivalent de cd : les instructions suivantes s’exécutent dans ce répertoire (créé si absent).
WORKDIR /app
COPY . . # vers /app
RUN npm install
Préférer un chemin absolu — les chemins relatifs s’empilent sur le WORKDIR précédent.
CMD vs ENTRYPOINT — commande de démarrage
CMD est remplaçable, ENTRYPOINT non.
CMD définit la commande par défaut :
CMD ["node", "server.js"]
docker run my-app → node server.js. docker run my-app npm test → CMD remplacé par npm test.
ENTRYPOINT fixe le processus principal :
ENTRYPOINT ["node"]
CMD ["server.js"]
docker run my-app → node server.js ; docker run my-app script.js → node script.js. ENTRYPOINT fixe, CMD ou les args de run complètent.
Quand utiliser quoi ?
- CMD seul : service web, démarrages variables (prod
npm start, testnpm test) - ENTRYPOINT + CMD : image outil (Python : ENTRYPOINT
python, CMD le script) - ENTRYPOINT seul : conteneur à usage unique
# Web (CMD)
FROM node:18-alpine
WORKDIR /app
COPY . .
CMD ["npm", "start"]
# Outil Python (ENTRYPOINT + CMD)
FROM python:3.11-slim
ENTRYPOINT ["python"]
CMD ["main.py"]
ENTRYPOINT = quoi faire, CMD = comment.
ENV — variables d’environnement
ENV définit des variables utilisables dans RUN, CMD et par l’application.
ENV NODE_ENV=production
ENV PORT=3000
RUN echo "Environment: $NODE_ENV"
CMD ["node", "server.js"]
Usages courants : NODE_ENV=production, étendre PATH, paramètres applicatifs.
Les valeurs ENV restent dans l’image — pas de secrets (mots de passe) : préférer docker run -e ou Docker Secrets.
Atelier — construire votre première image
Théorie passée, pratiquons avec une petite app Node.js.
Étape 1 : préparer le projet
mkdir my-node-app
cd my-node-app
package.json :
{
"name": "my-node-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2"
}
}
server.js :
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Hello from Docker!');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Étape 2 : écrire le Dockerfile
À la racine, fichier Dockerfile (sans extension) :
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Pourquoi séparer package.json et le code ? Cache Docker : chaque instruction = une couche. Si une couche change, les suivantes sont reconstruites. package.json change rarement, le code souvent. Tout copier puis npm install = réinstallation à chaque edit.
Avec l’ordre actuel, package.json stable → couche dépendances en cache → build bien plus rapide.
Étape 3 : construire l’image
docker build -t my-node-app:1.0 .
-t my-node-app:1.0: nom:tag.: contexte = répertoire courant
Chaque étape du log correspond à une instruction. Succès : Successfully built xxx.
Étape 4 : lancer le conteneur
docker run -p 3000:3000 my-node-app:1.0
-p 3000:3000: hôte:conteneur- Image :
my-node-app:1.0
Message attendu : « Server running on port 3000 ».
Étape 5 : vérifier
Ouvrir http://localhost:3000 — « Hello from Docker! » confirme le succès. Ctrl+C arrête le conteneur.
En résumé
- Code (package.json + server.js)
- Dockerfile (recette de build)
docker builddocker run
L’essentiel : rôle de chaque instruction, contexte de build et cache.
Pièges des débutants
Piège 1 : mauvais contexte de build
Symptôme : COPY — « no such file or directory » alors que le fichier existe.
Cause : chemins relatifs au contexte, pas au Dockerfile.
COPY ../config.json /app/
COPY /opt/myfile.txt /app/
Solutions :
- Fichiers dans le répertoire du projet
docker build -f subdir/Dockerfile .(contexte = parent)
Piège caché : docker build . à la racine envoie tout (node_modules, .git…) — j’ai déjà attendu 10 min sur des Go de contexte.
.dockerignore :
node_modules
.git
.env
*.log
Piège 2 : trop de couches, image énorme
Symptôme : quelques Mo de code, image en Go.
Cause : chaque RUN/COPY/ADD crée une couche ; suppression tardive ne retire pas les données des couches précédentes.
RUN apt-get update
RUN apt-get install -y curl
# ... plusieurs RUN ...
RUN rm tool.sh # inutile — tool.sh reste dans une couche antérieure
Solution : un seul RUN avec &&, installation, usage et nettoyage dans la même couche.
Sept RUN séparés → 2 Go ; fusion → 200 Mo (facteur ~10).
Piège 3 : cache des dépendances invalidé
Symptôme : réinstallation des dépendances à chaque build.
Cause : mauvais ordre COPY — code et dépendances ensemble.
COPY . .
RUN npm install
Toute modification de source invalide COPY → npm install relancé.
Solution :
COPY package*.json ./
RUN npm install
COPY . .
Modification du code seul ne relance pas npm install — de 5 min à ~10 s.
Note : EXPOSE n’est pas obligatoire
EXPOSE 3000 documente le port ; sans lui, docker run -p 3000:3000 my-app fonctionne. Mieux vaut toutefois le garder pour la lisibilité.
Conclusion
Trois points pour débuter :
- Instructions clés : FROM, RUN, COPY, CMD — WORKDIR et ENV en support
- Contexte de build : COPY limité au répertoire du
.dedocker build - Cache : dépendances avant le code, fusionner les RUN
Essayez sur un petit projet : Dockerfile minimal qui tourne, perfection plus tard (multi-stage, optimisation).
Docker demande de la pratique — ma première nuit d’erreurs, puis la logique est devenue claire. Vous en êtes capable.
Suite possible :
- Docker Compose (applications multi-conteneurs)
- Build multi-stage (réduire encore la taille)
- Réseau et volumes Docker
Bonne construction de votre première image ! Questions bienvenues en commentaires.
FAQ
Quelles sont les instructions essentielles d'un Dockerfile ?
1) FROM — choisir l'image de base (doit être la première)
2) RUN — exécuter des commandes pendant la construction (fusionner avec && pour réduire les couches)
3) COPY — copier des fichiers (chemins relatifs au contexte de build, pas ../ ni chemins absolus)
4) WORKDIR — définir le répertoire de travail (chemin absolu recommandé)
5) CMD — commande de démarrage du conteneur (peut être remplacée)
6) ENV — variables d'environnement
À connaître aussi : ENTRYPOINT (commande principale fixe), EXPOSE (déclaration de port, documentation uniquement).
Pourquoi COPY ../config.json provoque une erreur ?
Le contexte est le répertoire indiqué par le point (.) à la fin de docker build — COPY ne peut accéder qu'à ce répertoire et ses sous-dossiers, pas au parent ni aux chemins absolus.
Solutions :
• Déplacer le fichier dans le projet
• Ou ajuster la commande : docker build -f subdir/Dockerfile .
Créer un .dockerignore pour exclure node_modules, .git, etc. accélère le build.
Comment réduire la taille d'une image Docker ?
1) Images Alpine :
• ~5 Mo vs ~900 Mo pour une image complète — facteur ~180
2) Fusionner les instructions RUN :
• Enchaîner avec && : installation, utilisation et nettoyage dans la même couche
• Peut passer de 2 Go à 200 Mo — facteur ~10
3) Créer un .dockerignore pour exclure les fichiers inutiles
Toujours regrouper apt-get update et install — éviter un cache obsolète.
Comment exploiter le cache Docker pour accélérer le build ?
Bon ordre :
• Copier d'abord package*.json et installer les dépendances
• Puis copier le code source
• Si package.json ne change pas, la couche dépendances est réutilisée
• Build de 5 min à ~10 s (facteur ~30)
Mauvais ordre :
• Tout copier puis installer
• Chaque modification de code relance npm install — très lent
Quelle différence entre CMD et ENTRYPOINT ?
Cas d'usage :
1) CMD seul : service applicatif, démarrages différents (prod npm start, test npm test)
2) ENTRYPOINT+CMD : image outil, commande fixe et paramètres variables (script Python)
3) ENTRYPOINT seul : scénario très figé
À retenir : ENTRYPOINT = quoi faire, CMD = comment le faire.
EXPOSE est-il obligatoire ?
Le mapping réel se fait avec docker run -p. Même sans EXPOSE, docker run -p 3000:3000 my-app fonctionne.
Il est toutefois recommandé d'ajouter EXPOSE pour la lisibilité.
Quelle différence entre images Alpine et slim ?
• ~5 Mo, adapté à la production
• musl libc au lieu de glibc — certaines dépendances natives peuvent échouer
• Image complète ~900 Mo — facteur ~180
En cas d'erreur de compilation bizarre, passer à slim (ex. node:18-slim).
Principe : Alpine d'abord, slim si problème de compatibilité.
8 min de lecture · Publié le: 17 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide pratique Docker
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Guide d'installation Docker 2025 : de permission denied au fonctionnement réussi
WSL 2 sur Windows, version selon la puce sur Mac, permissions et dépendances sur Linux ? Ce guide regroupe 10+ erreurs d'installation Docker sur les trois plateformes avec leurs solutions — de permission denied à un Docker qui tourne.
Partie 2 sur 38
Suivant
Optimisation Dockerfile : 5 astuces pour réduire la taille de l'image de 80 %
Vos images Docker pèsent plusieurs Go ? Maîtrisez Alpine, la fusion des RUN, le build multi-étapes, .dockerignore et le nettoyage du cache — passez de 1,2 Go à 180 Mo (-85 %). Cas Node.js complet et données mesurées.
Partie 4 sur 38



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire