Dockerfile expliqué : construire une image web | Blog yunt
●Développement Web· Débutant
Dockerfile expliqué : construire une image web
Écrivez un Dockerfile fiable pour une application web : FROM, COPY, RUN, CMD, cache, .dockerignore et multi-stage, avec des exemples Node et Nginx commentés.
YYanis ELMALEH···7 min de lecture
Instruction
Moment
Exemple
`RUN`
Construction de l’image
`RUN npm ci`
`CMD`
Démarrage, valeurs par défaut
`CMD ["npm", "start"]`
`ENTRYPOINT`
Exécutable principal
`ENTRYPOINT ["docker-entrypoint.sh"]`
Y
Écrit par
Yanis ELMALEH
Un Dockerfile est une recette textuelle : une suite d’instructions qui indique à Docker comment construire une image. Pour une application web (Node, PHP ou Python), c’est le passage de « je lance une image officielle générique » à « voici mon image, avec mon code et mes dépendances ».
Ce guide détaille les instructions essentielles (FROM, COPY, RUN, CMD), le multi-stage, et les erreurs qui produisent des images lentes, lourdes ou risquées. La référence complète se trouve dans la documentation du Dockerfile.
À quoi sert un Dockerfile
Sans Dockerfile, vous utilisez surtout des images toutes faites (nginx, postgres). Dès que vous devez :
installer des paquets npm ou Composer ;
compiler un front (Vite, Next.js…) ;
figer la version exacte du runtime ;
embarquer votre code pour la CI ou la production ;
…vous écrivez un Dockerfile. L’image obtenue est un artefact partageable : même contenu sur votre ordinateur, en intégration continue et sur un serveur. Pour la construire :
example.shsh
docker build -t mon-app:dev .
L’option -t donne un nom et une étiquette (tag) à l’image. Le point final désigne le contexte de build : l’ensemble des fichiers envoyés au démon Docker. D’où l’importance du fichier .dockerignore, abordé plus bas.
Anatomie d’un Dockerfile Node
Voici un exemple pédagogique pour une API Express :
example.dockerfiledockerfile
FROM node:22-bookworm-slim
WORKDIR /usr/src/app
# Les manifestes d'abord, pour profiter du cache
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Le reste du code ensuite
COPY . .
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "server.js"]
FROM : l’image de base
Choisissez une étiquette précise (22-bookworm-slim) plutôt que latest, qui change dans le temps. Les variantes slim et alpine allègent l’image ; Debian slim est souvent plus confortable quand des dépendances natives sont en jeu.
WORKDIR : le répertoire de travail
Équivalent d’un cd permanent pour les instructions suivantes. Si le dossier n’existe pas, Docker le crée.
COPY : importer vos fichiers
COPY transfère des fichiers du contexte vers l’image. L’ordre compte pour le cache : copiez d’abord les manifestes de dépendances, lancez l’installation, puis copiez le reste du code. Modifier server.js ne relance alors pas npm ci.
RUN : exécuter pendant le build
RUN lance une commande au moment de la construction (installer des paquets, compiler). Chaque instruction crée une couche (layer) : regroupez avec && quand c’est pertinent, sans rendre la recette illisible.
ENV et USER : environnement et droits
ENV définit des variables d’environnement dans l’image, par exemple NODE_ENV. N’y gravez jamais de secret : une clé d’API écrite en clair reste lisible dans l’historique de l’image. USER abandonne les droits root ; les images Node officielles fournissent un utilisateur node pour cela.
EXPOSE : documenter le port
EXPOSE indique le port d’écoute, mais n’ouvre rien sur l’hôte : c’est l’option -p de docker run qui publie le port.
CMD : la commande de démarrage
CMD définit la commande lancée au démarrage du conteneur. Privilégiez la forme exec en JSON (["node", "server.js"]) : les signaux d’arrêt sont mieux transmis au processus.
Si votre conteneur s’arrête aussitôt, regardez CMD : quand le processus principal se termine, le conteneur aussi. Une API doit donc rester au premier plan.
.dockerignore : le détail qui change tout
À la racine du projet, ce fichier exclut des éléments du contexte :
Sans lui, vous envoyez le node_modules de votre machine (lent, parfois incompatible d’un système à l’autre) et vous risquez de copier un .env local dans l’image. C’est mauvais pour les performances et pour la sécurité.
Multi-stage : compiler sans alourdir la production
Les applications web modernes compilent souvent un artefact (JavaScript, CSS). La construction multi-stage sépare les outils de build de l’image finale. La documentation sur le multi-stage présente le principe en détail.
example.dockerfiledockerfile
# Étape de build : tous les outils nécessaires
FROM node:22-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Étape finale : uniquement ce qui sert à l'exécution
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Les avantages sont concrets : pas de compilateurs inutiles en production, une image finale plus petite et une surface d’attaque réduite. Pour un front purement statique, l’étape finale peut même être une image Nginx qui sert le dossier compilé.
Exemple : un front Vite servi par Nginx
example.dockerfiledockerfile
FROM node:22-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
Le fichier nginx.conf gère la règle try_files nécessaire au routage d’une application monopage. Le résultat : une image légère, un serveur éprouvé pour le statique et un build reproductible en CI. Retenez la règle : l’environnement qui construit n’est pas forcément celui qui exécute. Node compile, Nginx sert.
Optimiser le cache de build
L’ordre des instructions détermine ce que Docker peut réutiliser :
FROM et WORKDIR ;
copie de package.json et du verrou de dépendances ;
RUN npm ci ;
copie du code source ;
RUN npm run build, si nécessaire ;
USER et CMD.
Modifier package-lock.json recalcule les dépendances ; modifier un seul fichier de routes réutilise la couche d’installation. Sur une intégration continue, ce détail réduit sensiblement les temps de construction.
Déboguer un build qui échoue
example.shsh
docker build -t mon-app:debug .
Si l’échec survient pendant RUN npm ci, lisez d’abord les journaux de npm : registre inaccessible, dépendances incompatibles, bibliothèques système manquantes (certains modules natifs demandent des outils de compilation). Vous pouvez aussi construire jusqu’à une étape précise, puis y ouvrir un shell :
Copier tout avant d’installer les dépendances : chaque modification de code invalide le cache, et les builds deviennent lents.
Oublier le fichier de verrouillage : npm install sans lockfile laisse flotter les versions. Préférez npm ci avec un package-lock.json versionné.
Tourner en root : pratique pour déboguer, risqué en production. Passez à un utilisateur non privilégié.
Laisser des secrets dans les couches : un ENV API_KEY=… ou un COPY .env reste récupérable dans l’historique. Fournissez les secrets à l’exécution.
Utiliser latest sans le documenter : aujourd’hui l’image fonctionne, demain l’étiquette a bougé. Épinglez une version et mettez à jour volontairement.
Tout mettre dans une seule étape : dépendances de développement, outils de test et application de production dans la même image la rendent lourde et poreuse.
Bonnes pratiques
une image, une responsabilité principale ;
des étiquettes explicites (mon-app:1.4.2) ;
une route /health et, si besoin, une instruction HEALTHCHECK ;
des journaux sur la sortie standard plutôt que dans un fichier du conteneur ;
les variables d’environnement requises documentées dans le README ;
une analyse régulière des vulnérabilités de vos images, surtout si la base est ancienne.
Un Dockerfile décrit comment fabriquer une image web fiable : base claire, dépendances en cache, code copié au bon moment, processus non-root et commande de démarrage explicite. Le multi-stage sépare le build du runtime pour obtenir des images plus saines.
Écrivez une recette simple pour votre API, ajoutez un .dockerignore, étiquetez l’image et lancez-la avec un mappage de port. Quand c’est stable, passez à Compose. Pour transformer ces gestes en compétence durable, découvrez Academy Yunt.
FAQ
Quelle différence entre Dockerfile et docker-compose.yml ?
Le Dockerfile explique comment construire l’image d’une application. Compose décrit comment lancer plusieurs services ensemble, en s’appuyant sur ces images.
Faut-il utiliser Alpine ?
Ce n’est pas obligatoire. Alpine est compacte mais repose sur la bibliothèque musl, que certains modules natifs supportent mal. Debian slim est un excellent choix par défaut pour Node.
COPY ou ADD ?
Préférez COPY. ADD ajoute des comportements supplémentaires (téléchargement d’URL, extraction d’archives) qui surprennent souvent.
Où placer le Dockerfile dans un monorepo ?
Généralement à la racine de chaque application ou package, avec un contexte de build ciblé pour n’envoyer que les fichiers utiles.
Forme shell ou forme exec pour CMD ?
La forme exec JSON transmet mieux les signaux et évite un shell intermédiaire. Utilisez la forme shell seulement si vous avez besoin de l’expansion de variables par le shell.