📍 Article principal du parcours : Docker de zéro : comprendre et utiliser les conteneurs
Deuxième étape du parcours « Docker de zéro ». Si vous n’avez jamais lancé de conteneur, commencez par images et conteneurs : le modèle mental de Docker.
Jusqu’ici, vous avez lancé des images que d’autres ont construites. Le moment est venu de fabriquer la vôtre. Le Dockerfile est le fichier qui décrit, ligne par ligne, comment emballer votre application dans une image : de quoi partir, quoi copier, quoi installer, quoi lancer. C’est la pièce centrale de Docker — celle qui transforme un dossier de code source en un artefact reproductible que n’importe qui peut exécuter à l’identique. Vous allez écrire votre premier Dockerfile pour conteneuriser « Garde », la petite API du parcours.
🎯 Ce que vous allez apprendre
- écrire un
Dockerfilepropre pour une application Node.js ; - comprendre les instructions clés :
FROM,WORKDIR,COPY,RUN,EXPOSE,CMD; - construire l’image avec
docker buildet la lancer ; - exploiter le cache des couches pour des builds rapides en ordonnant correctement les instructions ;
- passer la configuration par variables d’environnement plutôt qu’en dur.
🛠️ Ce que vous allez construire
Une petite API « Garde » écrite avec Express : un serveur qui répond sur /pharmacies avec une liste au format JSON. Vous l’emballerez dans votre propre image garde-api, que vous lancerez comme n’importe quelle image officielle. À la fin, http://localhost:3000/pharmacies renverra les données depuis votre conteneur.
Prérequis
- Docker installé et fonctionnel (voir le tutoriel précédent).
- Un éditeur de texte (VS Code, par exemple).
- Aucune installation de Node n’est nécessaire sur votre machine — c’est tout l’intérêt.
- Niveau débutant. Test express : si vous savez créer un dossier et y placer des fichiers, vous êtes prêt.
- ⏱️ Temps estimé : ~40 minutes.
Étape 1 — Préparer le code de l’API
Avant de penser conteneur, il faut une application à conteneuriser. Créons un dossier garde-api avec deux fichiers : le manifeste package.json, qui déclare les dépendances, et server.js, le code du serveur. Restons volontairement minimalistes : l’objectif est Docker, pas Express.
// package.json
{
"name": "garde-api",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^5.1.0" }
}
// server.js
import express from "express";
const app = express();
const PORT = process.env.PORT || 3000;
const pharmacies = [
{ nom: "Pharmacie du Marché", quartier: "Centre", garde: true },
{ nom: "Pharmacie de la Gare", quartier: "Nord", garde: true },
{ nom: "Pharmacie de l'Avenue", quartier: "Sud", garde: false }
];
app.get("/pharmacies", (req, res) => {
res.json(pharmacies.filter((p) => p.garde));
});
app.get("/", (req, res) => res.send("API Garde en ligne"));
app.listen(PORT, () => console.log(`API Garde a l'ecoute sur le port ${PORT}`));
Le code lit le port dans la variable d’environnement PORT (avec 3000 par défaut) : c’est une bonne habitude qui rendra l’image configurable sans la reconstruire. La route /pharmacies renvoie uniquement les officines de garde. Rien de plus pour l’instant ; on branchera une vraie base de données dans un tutoriel ultérieur.
✅ Point d’étape — Vous avez un dossier
garde-api/contenantpackage.jsonetserver.js. Pas besoin de les exécuter maintenant : Docker s’en chargera.
Étape 2 — Écrire le Dockerfile
Place à la pièce maîtresse. Créez, à la racine de garde-api, un fichier nommé exactement Dockerfile (sans extension). Chaque ligne est une instruction qui produit une couche de l’image. Lisez les commentaires : ils expliquent le rôle de chaque instruction.
# Partir d'une image officielle Node.js, version LTS, variante allegee
FROM node:24-alpine
# Definir le repertoire de travail a l'interieur de l'image
WORKDIR /app
# Copier d'abord les manifestes de dependances (pour profiter du cache)
COPY package.json package-lock.json* ./
# Installer les dependances
RUN npm install --omit=dev
# Copier ensuite le reste du code
COPY . .
# Documenter le port sur lequel l'application ecoute
EXPOSE 3000
# Commande lancee au demarrage du conteneur
CMD ["node", "server.js"]
Détaillons les instructions, car ce sont elles que vous réutiliserez partout. FROM choisit l’image de départ — ici Node.js 24, version LTS active au moment d’écrire, dans sa variante alpine légère. WORKDIR fixe le répertoire courant pour toutes les instructions suivantes. COPY recopie des fichiers de votre machine vers l’image. RUN exécute une commande au moment de la construction (ici, l’installation des dépendances). EXPOSE documente le port (c’est purement indicatif, la publication réelle se fait au run). Enfin CMD définit la commande exécutée au lancement du conteneur.
✅ Point d’étape — Le fichier
Dockerfileest à la racine du dossier, à côté deserver.js. Vérifiez l’orthographe exacte : un fichier nommédockerfile.txtne sera pas reconnu.
Étape 3 — Comprendre l’ordre des instructions et le cache
Une question légitime se pose : pourquoi copier package.json avant le reste du code, en deux COPY séparés, plutôt qu’un seul COPY . . au début ? La réponse tient au cache des couches, et c’est l’optimisation la plus rentable que connaisse un débutant.
Docker met chaque couche en cache et la réutilise tant que son contenu n’a pas changé. Vos dépendances changent rarement ; votre code, lui, change à chaque modification. En copiant d’abord package.json puis en lançant npm install, cette étape coûteuse n’est rejouée que si package.json a réellement été modifié. Quand vous ne touchez qu’à server.js, Docker réutilise la couche d’installation depuis le cache et ne refait que la copie du code — un build de quelques secondes au lieu de plusieurs minutes. Inverser l’ordre casserait ce bénéfice : le moindre changement de code invaliderait l’installation. Ce principe — du plus stable au plus volatil — guide l’écriture de tout bon Dockerfile.
Étape 4 — Ignorer ce qui ne doit pas entrer dans l’image
Le COPY . . recopie tout le dossier, y compris des éléments qui n’ont rien à faire dans l’image : le dossier node_modules local, les fichiers Git, les journaux. Un fichier .dockerignore, jumeau du .gitignore, les exclut. Cela allège l’image et accélère le build en réduisant le « contexte » envoyé au daemon.
# .dockerignore
node_modules
npm-debug.log
.git
.gitignore
Dockerfile
.dockerignore
*.md
Sans ce fichier, un node_modules local de plusieurs centaines de méga-octets serait copié inutilement, puis écrasé par le npm install du build — du gaspillage pur. Avec lui, le contexte de build reste minuscule. Sur une connexion lente, la différence de temps de build est immédiatement sensible.
✅ Point d’étape — Un fichier
.dockerignoreexiste à la racine. Le dossier contient maintenant :server.js,package.json,Dockerfile,.dockerignore.
Étape 5 — Construire l’image
Tout est prêt pour la construction. La commande docker build lit le Dockerfile, exécute chaque instruction et produit une image étiquetée. L’option -t donne un nom (un tag) à l’image ; le . final indique que le contexte de build est le dossier courant.
docker build -t garde-api:1.0 .
Vous voyez défiler les étapes, chacune correspondant à une instruction du Dockerfile. La première fois, Docker télécharge l’image de base node:24-alpine et installe les dépendances ; les fois suivantes, il réutilise le cache. À la fin, « writing image » puis « naming to … garde-api:1.0 » confirment le succès. Vérifiez avec docker images : votre image garde-api figure désormais dans la liste, aux côtés des images officielles.
✅ Point d’étape —
docker imagesaffiche une lignegarde-api 1.0. Votre première image maison existe.
Étape 6 — Lancer et vérifier votre image
Une image ne sert à rien tant qu’on ne l’a pas exécutée. Lançons un conteneur à partir de garde-api:1.0, en publiant le port et en lui donnant un nom, exactement comme on l’a fait avec nginx.
docker run -d --name garde -p 3000:3000 garde-api:1.0
On retrouve exactement les options du tutoriel précédent : -d pour l’arrière-plan, --name garde pour un nom lisible, -p 3000:3000 pour relier le port hôte au port du conteneur. Une fois lancé, ouvrez http://localhost:3000/pharmacies : votre navigateur affiche la liste JSON des pharmacies de garde, servie par votre propre conteneur. Consultez docker logs garde pour voir le message « API Garde à l’écoute sur le port 3000 ». Vous avez bouclé le cycle complet : du code source à une image, puis à un conteneur en marche.
✅ Point d’étape —
http://localhost:3000/pharmaciesrenvoie un tableau JSON de deux pharmacies de garde. L’API tourne depuis votre image.
Étape 7 — Rendre l’image configurable
Le code lit déjà la variable PORT. Prouvons que l’image est configurable sans la reconstruire, en lançant un second conteneur sur un autre port interne, via l’option -e.
docker run -d --name garde-test -e PORT=4000 -p 4001:4000 garde-api:1.0
Ici, l’application écoute sur le port 4000 dans le conteneur (parce qu’on lui passe PORT=4000), et ce port est publié sur 4001 côté hôte. Ouvrez http://localhost:4001/pharmacies : la même image sert deux configurations différentes. C’est le principe à retenir — une image, plusieurs configurations par environnement — qui rend les conteneurs si pratiques pour passer du développement à la production sans rien reconstruire.
✅ Point d’étape — Deux conteneurs issus de la même image répondent sur deux ports, avec des configurations distinctes. Nettoyez ensuite avec
docker rm -f garde garde-test.
Comprendre le contexte de build et les couches
Deux notions méritent d’être clarifiées, car elles expliquent bien des comportements déroutants. La première est le contexte de build : le point final de docker build -t garde-api:1.0 . n’est pas décoratif. Il désigne le dossier dont le contenu est envoyé au daemon Docker pour servir de matière à la construction. Tout ce que COPY peut recopier doit se trouver dans ce contexte. C’est aussi pourquoi un gros node_modules ou un dossier .git volumineux ralentit le build même s’ils ne sont pas copiés : ils gonflent le contexte transféré. Le .dockerignore agit précisément là, en réduisant ce qui est envoyé.
La seconde notion est l’empilement des couches. Chaque instruction du Dockerfile — FROM, COPY, RUN — produit une couche en lecture seule, posée sur la précédente. L’image finale est cette pile. Quand vous lancez un conteneur, Docker ajoute par-dessus une mince couche écrivable, propre au conteneur. C’est ce modèle qui rend les images si efficaces à stocker et à transférer : deux images partant de node:24-alpine partagent physiquement la même couche de base sur le disque, sans la dupliquer. Et lors d’un build, tant qu’une instruction et tout ce qui la précède n’ont pas changé, Docker réutilise la couche en cache au lieu de la recalculer. Vous verrez « CACHED » défiler à l’écran : c’est le signe que le cache a joué.
De ce modèle découle une mise en garde de sécurité importante, qu’on oublie souvent. Une couche est immuable et conserve tout ce qui y a été ajouté, même si une instruction ultérieure semble l’effacer. Concrètement, si vous écrivez un mot de passe ou une clé dans un fichier à une étape, puis le supprimez à l’étape suivante, le secret reste présent dans la couche intermédiaire — récupérable par quiconque inspecte l’image. La règle est donc absolue : jamais de secret en dur dans un Dockerfile. Les valeurs sensibles se passent à l’exécution, par variables d’environnement ou fichiers montés, comme vous l’avez fait avec PORT et comme vous le ferez avec les identifiants de base de données.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| « failed to read dockerfile » | Fichier mal nommé ou absent | Le nommer exactement Dockerfile, à la racine du contexte de build. |
Le build recopie un énorme node_modules |
Pas de .dockerignore |
Ajouter node_modules au .dockerignore. |
npm install rejoué à chaque build |
Code copié avant les manifestes | Copier package.json et lancer npm install avant COPY . .. |
| Le conteneur démarre puis s’arrête | CMD incorrecte ou erreur au démarrage |
Lire docker logs ; vérifier que node server.js tourne. |
| « Cannot find module ‘express’ » | Dépendances non installées dans l’image | Vérifier qu’express est dans package.json et que RUN npm install figure avant CMD. |
Des builds rapides quand la bande passante est comptée
Sur une connexion partagée à Ouagadougou, le premier build est le plus coûteux : il télécharge node:24-alpine et toutes les dépendances. Là encore, deux réflexes font la différence. D’abord, le bon ordre des instructions : une fois les dépendances en cache, vos itérations sur le code se construisent en quelques secondes sans retoucher au réseau. Ensuite, le choix de la base alpine : l’image finale de garde-api tourne autour de 150 Mo, là où une base Node complète ferait facilement plusieurs fois cette taille — un poids qui comptera quand vous la pousserez sur un registre et la tirerez sur un serveur. Pensez aussi à épingler les versions (node:24-alpine et non node:latest) : sur un projet partagé entre plusieurs développeurs aux connexions inégales, la reproductibilité évite les « chez moi le build passe » insolubles.
✅ Récapitulatif
Vous savez écrire un Dockerfile, en comprendre les instructions essentielles, exploiter le cache des couches en ordonnant du plus stable au plus volatil, exclure le superflu avec .dockerignore, construire avec docker build -t et lancer votre image en la configurant par variables d’environnement. L’API « Garde » vit maintenant dans une image que vous maîtrisez de bout en bout. Prochaine étape : lui adjoindre une vraie base de données, et orchestrer les deux avec Docker Compose plutôt qu’à coups de longues commandes.
🧾 Aide-mémoire
| Instruction / commande | Rôle |
|---|---|
FROM image:tag |
Image de base de départ |
WORKDIR /app |
Répertoire de travail dans l’image |
COPY src dest |
Copier des fichiers dans l’image |
RUN commande |
Exécuter une commande au build (ex. installer) |
EXPOSE 3000 |
Documenter le port d’écoute |
CMD ["node","server.js"] |
Commande au démarrage du conteneur |
docker build -t nom:tag . |
Construire l’image depuis le dossier courant |
💪 À vous de jouer
Ajoutez une route /sante qui renvoie { "statut": "ok" }, reconstruisez l’image en version 1.1, et vérifiez que la nouvelle route répond. Observez quelles étapes du build sont rejouées et lesquelles viennent du cache.
Voir une solution
// Ajouter dans server.js, avant app.listen :
app.get("/sante", (req, res) => res.json({ statut: "ok" }));
docker build -t garde-api:1.1 .
docker run -d --name garde11 -p 3000:3000 garde-api:1.1
# Tester http://localhost:3000/sante
Comme seul server.js a changé, l’étape npm install vient du cache (« CACHED ») et seule la copie du code est rejouée. C’est le cache des couches en action.
Tutoriels frères
- Images et conteneurs — les gestes de base avant de construire.
- Débuter avec Docker Compose — orchestrer l’API et sa base ensemble.
Pour approfondir
- 🔝 Retour au guide principal : Docker de zéro : comprendre et utiliser les conteneurs
- Documentation officielle : référence du Dockerfile.
- Pour des images de production minimales, voir les builds multi-étapes.
Ressources et références
- Référence officielle du Dockerfile
- Bonnes pratiques de build Docker
- Image officielle Node.js sur Docker Hub
FAQ
Quelle différence entre CMD et ENTRYPOINT ?
Les deux définissent ce qui s’exécute au démarrage. CMD fournit une commande par défaut, facilement remplaçable au docker run. ENTRYPOINT fixe un exécutable plus rigide, auquel les arguments du run s’ajoutent. Pour débuter, CMD suffit dans l’immense majorité des cas.
Pourquoi alpine plutôt qu’une image Node classique ?
Alpine Linux est une distribution minimale ; l’image résultante est bien plus légère, donc plus rapide à télécharger et à déployer. Dans de rares cas, une dépendance native exige des bibliothèques absentes d’Alpine ; on bascule alors sur la variante node:24-slim, intermédiaire. Pour une API simple comme « Garde », alpine convient parfaitement.
Dois-je copier package-lock.json ?
Oui, dès qu’il existe : il fige les versions exactes des dépendances et garantit des installations identiques d’un build à l’autre. L’étoile dans package-lock.json* évite simplement une erreur si le fichier n’a pas encore été généré.
Le tag de version, à quoi sert-il vraiment ?
Il identifie une version précise de votre image (garde-api:1.0, garde-api:1.1). Cela permet de revenir en arrière en cas de problème, de déployer une version connue, et d’éviter les surprises de :latest qui désigne une cible mouvante.