DEV Community

Cover image for J'ai écrit le Dockerfile que TOUT débutant écrit — puis je l'ai détruit erreur par erreur
Roméo DOSsOu
Roméo DOSsOu

Posted on

J'ai écrit le Dockerfile que TOUT débutant écrit — puis je l'ai détruit erreur par erreur

J'ai écrit le Dockerfile que TOUT débutant écrit — puis je l'ai détruit erreur par erreur

Il y a quelques semaines, j'ai écrit mon premier vrai Dockerfile "instinctif" — celui que n'importe qui produit en suivant juste sa logique, sans connaître les bonnes pratiques. Et il fonctionnait. L'application démarrait, je voyais ma page web, j'étais content.

Sauf que ce Dockerfile pesait 1,1 Go, exposait une clé API en clair, et faisait tourner mon code avec les pleins pouvoirs administrateur. Sans le savoir.

Cet article raconte comment je suis passé de ce Dockerfile naïf à une version professionnelle de 90 Mo, en corrigeant 7 erreurs une par une — avec, à chaque étape, la preuve concrète du problème avant la solution.

Si tu n'as jamais touché Docker, tu peux suivre cet article sans aucun prérequis. Si tu en as déjà fait, tu vas probablement reconnaître au moins 3 de ces erreurs dans tes propres projets (je ne juge pas, je les ai toutes faites).

Le point de départ : le Dockerfile qu'on écrit tous

Voici, mot pour mot, le genre de Dockerfile qu'on produit naturellement quand on découvre Docker :

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
ENV API_KEY=mon_super_secret_123
EXPOSE 3000
CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

Sept lignes. Ça marche. Et ça contient sept problèmes différents. Voyons-les un par un.

Erreur n°1 et 2 : l'ordre des instructions détruit le cache

Docker construit une image en couches. Chaque ligne du Dockerfile = une couche. Et Docker met en cache les couches qui n'ont pas changé, pour ne pas refaire un travail inutile.

Le souci avec COPY . . placé avant RUN npm install : si tu modifies un seul fichier de ton code, Docker considère que la couche COPY . . a changé — et il refait tout ce qui suit, y compris npm install, qui pourtant n'a rien à voir avec ton changement.

Résultat concret : 25 à 30 secondes perdues à chaque rebuild, pour rien.

La correction :

FROM node:20
WORKDIR /app

# On copie SEULEMENT les fichiers de dépendances d'abord
COPY package.json package-lock.json ./
RUN npm install

# Le code source, qui change souvent, vient APRÈS
COPY . .
Enter fullscreen mode Exit fullscreen mode

Après cette correction, modifier ton code source ne déclenche plus jamais un npm install inutile. La ligne apparaît en cache (CACHED) à chaque rebuild, tant que tu ne touches pas à package.json.

La règle à retenir : dans un Dockerfile, on place ce qui change rarement en haut, et ce qui change souvent en bas.

Erreur n°3 : pas de .dockerignore

Sans ce fichier, l'instruction COPY . . copie absolument tout ce qui se trouve dans ton dossier de projet — y compris des choses que tu ne voudrais jamais voir dans une image : node_modules (inutile, il est recréé par npm install), .git (tout ton historique de commits), et surtout un éventuel fichier .env contenant tes mots de passe locaux.

J'ai testé en créant un faux .env avec un mot de passe factice, puis j'ai lancé :

docker run --rm mon-app ls -la /app
Enter fullscreen mode Exit fullscreen mode

Le fichier .env apparaissait dans la liste. Sans aucune protection.

La correction, un fichier .dockerignore classique :

node_modules/
.git/
.env
*.log
tests/
Enter fullscreen mode Exit fullscreen mode

Même logique qu'un .gitignore, mais pour Docker.

Erreur n°4 : la démonstration qui m'a le plus marqué — les secrets immuables

Celle-ci vaut la peine d'être détaillée parce qu'elle m'a vraiment surpris.

J'ai écrit ceci dans mon Dockerfile :

ENV API_KEY=mon_super_secret_123
RUN unset API_KEY
Enter fullscreen mode Exit fullscreen mode

Mon intuition de débutant : "j'ai supprimé la variable juste après, donc elle ne devrait plus être visible". Faux.

docker history --no-trunc mon-app
Enter fullscreen mode Exit fullscreen mode

Le secret apparaît, en clair, dans l'historique des couches. Pour toujours. Parce que chaque couche Docker est immuable — une fois écrite, elle ne disparaît jamais, même si une couche suivante "annule" son effet visible.

C'est exactement le genre de vulnérabilité qu'on retrouve régulièrement dans des images publiées par erreur sur des registries publics, avec des clés AWS ou des tokens GitHub encore lisibles dans l'historique.

La correction : ne jamais écrire de valeur sensible dans le Dockerfile. On déclare la variable vide, et on injecte sa vraie valeur au moment du docker run :

ENV API_KEY=""
Enter fullscreen mode Exit fullscreen mode
docker run --env-file .env.runtime mon-app
Enter fullscreen mode Exit fullscreen mode

Le fichier .env.runtime ne quitte jamais ta machine — il n'est jamais copié dans l'image.

Erreur n°5 : tourner en root

Par défaut, si tu ne précises rien, ton application tourne avec l'utilisateur root — l'équivalent administrateur sous Linux. Si quelqu'un trouve une faille dans ton code et l'exploite, il obtient directement les pleins pouvoirs sur le conteneur.

La correction :

RUN useradd -m -s /bin/sh appuser
RUN chown -R appuser:appuser /app
USER appuser
Enter fullscreen mode Exit fullscreen mode

Après ça, docker run mon-app whoami répond appuser au lieu de root. C'est le principe du moindre privilège — un classique de la sécurité système, appliqué en deux lignes.

Erreurs n°6 et 7 : une image dix fois trop lourde

Mon image naïve pesait 1,1 Go. Pour comprendre pourquoi, il faut comprendre ce que contient réellement node:20 : un système Debian complet, avec des centaines d'outils dont mon application n'utilise jamais 1%.

Première correction, changer l'image de base :

FROM node:20-alpine
Enter fullscreen mode Exit fullscreen mode

Alpine est une distribution Linux minimaliste. Ce seul changement m'a fait passer de 1,1 Go à environ 180 Mo.

Deuxième correction, le multi-stage build — la technique la plus puissante de tout ce projet. L'idée : séparer la phase de construction de l'application (qui a besoin de tous les outils) de la phase d'exécution (qui n'a besoin que du résultat final).

# Stage 1 : on construit, avec tous les outils nécessaires
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --omit=dev
COPY . .

# Stage 2 : on ne garde que le strict nécessaire
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/app.js ./app.js
COPY --from=builder /app/package.json ./package.json
USER appuser
CMD ["node", "app.js"]
Enter fullscreen mode Exit fullscreen mode

Résultat final : environ 90 Mo. Une réduction de 92% par rapport au point de départ.

Le bilan complet

Erreur Risque Gain après correction
Mauvais ordre des couches Rebuild lent à chaque changement 30s → 0,1s
Pas de .dockerignore Fichiers sensibles copiés Image plus légère et sûre
Secrets en dur Fuite de clés API permanente Zéro secret dans l'image
Exécution en root Accès total en cas de faille Utilisateur limité
Image lourde 1,1 Go pour rien -80% avec Alpine
Pas de multi-stage Outils de build embarqués -92% au total

Ce que j'en retiens

La leçon la plus importante de tout ce projet : un Dockerfile qui fonctionne et un Dockerfile qui est bien fait sont deux choses complètement différentes. La première version naïve ne plante jamais — c'est justement ce qui la rend dangereuse, parce qu'on ne voit aucun signal d'alarme.

J'ai documenté l'intégralité de ce projet, avec le code source testable des deux versions (avant/après) et un guide pas-à-pas niveau zéro absolu, dans ce dépôt GitHub :

👉 https://github.com/Bordley/mon-app.git

Si tu repères d'autres erreurs classiques que je n'ai pas couvertes, ou si tu veux discuter d'un point précis, les commentaires sont ouverts.


Top comments (0)