Docker pour les devs - 04 - Écrire un Dockerfile

Les instructions du Dockerfile expliquees une par une, avec un exemple complet pour une app Bun/Node.

  1. 01 Docker pour les devs - 00 - Pourquoi Docker change tout
  2. 02 Docker pour les devs - 01 - Containers vs VMs
  3. 03 Docker pour les devs - 02 - L'architecture de Docker
  4. 04 Docker pour les devs - 03 - Docker Desktop, Engine et alternatives
  5. 05 Docker pour les devs - 04 - Écrire un Dockerfile
  6. 06 Docker pour les devs - 05 - Layers et cache
  7. 07 Docker pour les devs - 06 - Le .dockerignore
  8. 08 Docker pour les devs - 07 - Multi-stage builds
  9. 09 Docker pour les devs - 08 - Choisir son image de base
  10. 10 Docker pour les devs - 09 - Docker Compose, les bases
  11. 11 Docker pour les devs - 10 - Docker Compose avance
  12. 12 Docker pour les devs - 11 - Networking Docker, les bases
  13. 13 Docker pour les devs - 12 - Networking Docker avance
  14. 14 Docker pour les devs - 13 - Volumes et persistance
  15. 15 Docker pour les devs - 14 - Variables d'environnement et secrets
  16. 16 Docker pour les devs - 15 - Permissions et utilisateurs
  17. 17 Docker pour les devs - 16 - Docker et monorepo
  18. 18 Docker pour les devs - 17 - Dev vs Prod
  19. 19 Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation
  20. 20 Docker pour les devs - 19 - Debugger ses conteneurs
  21. 21 Docker pour les devs - 20 - Bases de donnees dans Docker
  22. 22 Docker pour les devs - 21 - Sauvegardes et restauration
  23. 23 Docker pour les devs - 22 - Conteneuriser un frontend
  24. 24 Docker pour les devs - 23 - Sécurité des conteneurs
  25. 25 Docker pour les devs - 24 - Optimisation des images
  26. 26 Docker pour les devs - 25 - Builds multi-platform
  27. 27 Docker pour les devs - 26 - Limiter les ressources de tes conteneurs
  28. 28 Docker pour les devs - 27 - Gerer les logs comme un adulte
  29. 29 Docker pour les devs - 28 - Healthchecks et restart policies
  30. 30 Docker pour les devs - 29 - Nettoyer Docker avant qu'il mange ton disque
  31. 31 Docker pour les devs - 30 - Registries et stratégie de tags
  32. 32 Docker pour les devs - 31 - Docker en CI/CD
  33. 33 Docker pour les devs - 32 - Au-dela de Compose
  34. 34 Docker pour les devs - 33 - Glossaire Docker de A a Z

04 - Écrire un Dockerfile

Ce que tu vas apprendre

  • Chaque instruction du Dockerfile et son rôle
  • La différence entre CMD et ENTRYPOINT
  • La différence entre COPY et ADD
  • ARG vs ENV
  • Un Dockerfile complet pour une app Bun/Node

Prerequisites


Mon premier Dockerfile, c'etait un desastre

Mon premier Dockerfile faisait 40 lignes, installait curl, wget, vim, htop, et lanceait l'app avec npm start en root. L'image faisait 1.8 Go. Mon collegue a regarde ca et m'a dit : "C'est pas un conteneur, c'est un serveur de dev." Il avait raison.

Un bon Dockerfile est minimaliste. Il contient ce dont l'application a besoin pour tourner et rien d'autre. Chaque instruction compte.

FROM : le point de depart

Tout Dockerfile commence par FROM. C'est l'image de base sur laquelle tu construis.

dockerfileFROM node:22-slim

Cette ligne dit : "pars de l'image officielle Node.js 22, variante slim." On verra les différentes variantes dans l'article 08.

Tu peux aussi partir de zero :

dockerfileFROM scratch

scratch est une image vide. Utile pour les binaires Go compiles statiquement. Pas pour Node.

WORKDIR : le répertoire de travail

dockerfileWORKDIR /app

WORKDIR créé le répertoire s'il n'existe pas et s'y place. Toutes les instructions suivantes s'executent depuis ce répertoire. Sans WORKDIR, tout se passe a la racine /, ce qui est sale.

COPY vs ADD

dockerfileCOPY package.json package-lock.json ./
COPY . .

COPY copie des fichiers depuis ton build context vers l'image. C'est l'instruction que tu utiliseras 99% du temps.

ADD fait la meme chose, plus deux extras :

  • Elle decompresse automatiquement les archives .tar.gz
  • Elle peut telecharger des fichiers depuis une URL
dockerfile# Decompresse l'archive dans /app
ADD archive.tar.gz /app/

# Telecharge depuis une URL (mauvaise pratique)
ADD https://example.com/file.txt /app/

La recommandation officielle : utilise COPY sauf si tu as besoin de la decompression automatique. ADD avec des URLs est déconseillé parce que ca créé une couche non cacheable.

RUN : exécuter des commandes

dockerfileRUN apt-get update && apt-get install -y \
    curl \
    && rm -rf /var/lib/apt/lists/*

RUN exécuté une commande dans le conteneur au moment du build. Chaque RUN créé une nouvelle couche dans l'image.

Le && est la pour chainer les commandes dans une seule couche. Si tu separes en plusieurs RUN, tu obtiens plus de couches et une image plus grosse :

dockerfile# Mauvais : 3 couches
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# Bon : 1 couche
RUN apt-get update && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*

Le rm -rf /var/lib/apt/lists/* nettoie le cache apt dans la meme couche. Si tu le fais dans un RUN séparé, les fichiers existent toujours dans la couche précédente.

ARG vs ENV

dockerfileARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV

ARG definit une variable disponible uniquement au moment du build. Elle disparaît du conteneur final.

ENV definit une variable d'environnement permanente, presente au build et au runtime.

bash# Surcharger un ARG au build
docker build --build-arg NODE_ENV=development -t mon-app .

Piege classique : ne mets jamais de secrets dans ARG ou ENV. Ils sont visibles dans l'historique de l'image (docker history).

EXPOSE : documenter les ports

dockerfileEXPOSE 3000

EXPOSE ne publie pas le port. Il sert de documentation pour indiquer quel port l'application écoûte. Le mapping réel se fait au docker run :

bash# -p hote:conteneur
docker run -p 8080:3000 mon-app

USER : ne pas tourner en root

dockerfileRUN addgroup --system app && adduser --system --ingroup app app
USER app

Par défaut, tout tourne en root dans un conteneur. C'est un risque de sécurité. USER change l'utilisateur pour les instructions suivantes et pour le runtime.

CMD vs ENTRYPOINT

C'est la source de confusion numero un.

CMD definit la commande par défaut. Elle peut etre surchargee au docker run.

dockerfileCMD ["node", "server.js"]
bash# Lance node server.js
docker run mon-app

# Surcharge : lance un shell
docker run mon-app sh

ENTRYPOINT definit l'executable principal. Les arguments du docker run sont passes en paramètres.

dockerfileENTRYPOINT ["node"]
CMD ["server.js"]
bash# Lance node server.js
docker run mon-app

# Lance node --inspect server.js
docker run mon-app --inspect server.js

Forme exec vs forme shell :

dockerfile# Forme exec (recommandee) - le processus recoit les signaux
CMD ["node", "server.js"]

# Forme shell - lance /bin/sh -c "node server.js"
CMD node server.js

La forme shell lance un shell intermediaire. Ton processus Node ne sera pas PID 1 et ne recevra pas les signaux SIGTERM pour un arrêt propre. Utilise toujours la forme exec.

LABEL : les metadonnees

dockerfileLABEL org.opencontainers.image.source="https://github.com/user/repo"
LABEL org.opencontainers.image.description="Mon API"
LABEL maintainer="nicolas@example.com"

Les labels ajoutent des metadonnees a l'image. C'est utile pour le tracking et l'automatisation.

Exemple complet : app Bun

Voici un Dockerfile realiste pour une API Bun :

dockerfileFROM oven/bun:1.1-slim AS base

WORKDIR /app

# Copier d'abord les fichiers de dependances (cache)
COPY package.json bun.lockb ./

# Installer les dependances
RUN bun install --frozen-lockfile --production

# Copier le code source
COPY src/ ./src/

# Creer un utilisateur non-root
RUN addgroup --system app && adduser --system --ingroup app app
USER app

EXPOSE 3000

CMD ["bun", "run", "src/index.ts"]

Et pour une app Node.js équivalente :

dockerfileFROM node:22-slim

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY src/ ./src/

RUN addgroup --system app && adduser --system --ingroup app app
USER app

EXPOSE 3000

CMD ["node", "src/index.js"]

Construire et lancer :

bashdocker build -t mon-api .
docker run -d -p 3000:3000 mon-api
curl http://localhost:3000

Sur paltemps.fr, chaque service a un Dockerfile qui suit ce schema. Les dépendances d'abord, le code ensuite, un utilisateur non-root, et la forme exec pour la commande.

Les erreurs courantes

  • Oublier le .dockerignore : ton node_modules de 500 Mo est envoye au daemon. Voir l'article 06.
  • Copier tout avant d'installer les deps : COPY . . avant npm install invalide le cache a chaque changement de code.
  • Tourner en root : ca marche, mais c'est un risque.
  • Utiliser latest : ton build n'est plus reproductible. Epingle une version.
  • Installer des outils de debug en prod : vim, curl, htop n'ont rien a faire dans une image de production.

Résumé

  • FROM definit l'image de base, WORKDIR le répertoire de travail
  • COPY pour les fichiers, RUN pour les commandes, && pour grouper les couches
  • CMD est surchargeable, ENTRYPOINT ne l'est pas (ou difficilement)
  • Toujours la forme exec ["cmd", "arg"] pour recevoir les signaux
  • USER pour ne pas tourner en root
  • ARG pour le build, ENV pour le runtime

Precedent : 03 - Desktop & Engine | Suivant : 05 - Layers & cache

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.