Changer le thème

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

Easton editorial illustration: image-layer stack

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 ?

180×
Écart de taille
Alpine vs image complète
10×
Réduction volume
Fusion des RUN
30×
Accélération build
5 min → 10 s
Source: Données mesurées

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 :

  1. Choisir une base (ex. Node.js 18)
  2. Y installer votre code et vos dépendances
  3. 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. 1

    Step 1: Étape 1 : Choisir l’image de base (FROM)

    FROM doit être la première instruction — choisir la bonne base :
  2. 2

    Step 2: • Node.js

    node:18-alpine (~5 Mo)
  3. 3

    Step 3: • Python

    python:3.11-slim
  4. 4

    Step 4: • Nginx

    nginx:alpine
  5. 5

    Step 5: Étape 2 : Répertoire de travail (WORKDIR)

    WORKDIR /app
  6. 6

    Step 6: Étape 3 : Dépendances (COPY+RUN)

    Copier d’abord package*.json, puis npm install
  7. 7

    Step 7: Mauvais ordre

    tout copier puis installer — chaque changement de code relance npm install.
  8. 8

    Step 8: Étape 4 : Code applicatif (COPY)

    Copier les sources
  9. 9

    Step 9: Étape 5 : Port (EXPOSE)

    EXPOSE 3000 — documentation uniquement
  10. 10

    Step 10: • Le mapping réel

    docker run -p
  11. 11

    Step 11: Étape 6 : Démarrage (CMD)

    CMD [“npm”, “start”] — remplaçable par docker run
  12. 12

    Step 12: • CMD seul

    service avec démarrages variables
  13. 13

    Step 13: • ENTRYPOINT+CMD

    outil, commande fixe et args variables
  14. 14

    Step 14: • ENTRYPOINT seul

    scénario très figé
  15. 15

    Step 15: Étape 7 : Build et run

    Build : docker build -t my-app:1.0 .
  16. 16

    Step 16: Run

    docker run -p 3000:3000 my-app:1.0
  17. 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-appnode 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-appnode server.js ; docker run my-app script.jsnode 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, test npm 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é

  1. Code (package.json + server.js)
  2. Dockerfile (recette de build)
  3. docker build
  4. docker 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 :

  1. Fichiers dans le répertoire du projet
  2. 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).

10×
Réduction de volume
Source: Fusion RUN : 2 Go → 200 Mo

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 :

  1. Instructions clés : FROM, RUN, COPY, CMD — WORKDIR et ENV en support
  2. Contexte de build : COPY limité au répertoire du . de docker build
  3. 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 ?
6 instructions clés :
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 ?
Les chemins COPY sont relatifs au contexte de build, pas au Dockerfile.

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 ?
Trois méthodes :

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 ?
Règle : placer en premier ce qui change peu, en dernier ce qui change souvent.

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 ?
CMD peut être remplacé par les arguments de docker run ; ENTRYPOINT non.

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 ?
Non. EXPOSE est une documentation — il indique quel port utilise l'image, le conteneur tourne sans.

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 ?
Alpine repose sur Alpine Linux :
• ~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

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog