FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT
Construisez vos propres images Docker optimisées
1. Comprendre le rôle de chaque instruction
FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT
2. Écrire un Dockerfile pour Node.js
Application Express typique
3. Comprendre les layers et le cache
Ordre des instructions, rebuilds accélérés
4. Optimiser un Dockerfile
Images minimales, .dockerignore, sécurité
5. Appliquer les bonnes pratiques de sécurité
Utilisateur non-root, images officielles
Rappel : images vs conteneurs
docker run, aujourd'hui on crée nos propres images
Le Dockerfile
Un fichier texte instruction par instruction
Les instructions clés
FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT, ARG, ENV
Layers et cache
Chaque instruction crée un layer, le cache accélère les rebuilds
Live coding : app Node.js/Express
Impact de l'ordre des instructions sur le cache
Bonnes pratiques
.dockerignore, images minimales, utilisateur non-root
Les bases avant de construire
📦 Image
Le modèle / le plan
Template en lecture seule
docker pull node:20
▶️ Conteneur
L'instance / l'objet
Process en cours d'exécution
docker run node:20
💡 Aujourd'hui on crée NOS PROPRES images avec un Dockerfile
Jusqu'ici vous utilisiez des images toutes faites (node, mysql, nginx...)
La recette de votre image
# Dockerfile minimal
FROM node:20-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Fichier texte → Build → Image → docker run
Chaque ligne est une instruction exécutée dans l'ordre
FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT
FROM node:20-alpine
FROM ubuntu:22.04
FROM python:3.12-slim
💡 Toujours partir d'une image officielle et taguée
Évitez latest — préférez un tag explicite (20, 22.04...)
Multi-stage build
On peut avoir plusieurs FROM dans un même Dockerfile
WORKDIR /app
✅ Ce que ça fait :
cd à répétition⚠️ Sans WORKDIR, on est à la racine (/)
COPY package.json .
COPY . .
COPY --chown=node:node . .
Syntaxe
COPY <src> <dest>
src = hôte, dest = dans l'image
Ordre crucial
Copier d'abord les fichiers qui changent peu (package.json)
❌ Évitez COPY . . trop tôt
Invalide tout le cache au moindre changement
RUN npm install
RUN apt-get update && apt-get install -y curl
RUN npm run build
Bonnes pratiques :
&&apt-get update && apt-get install -y --no-install-recommends && apt-get clean✅ Chaîner réduit le nombre de layers et la taille finale
EXPOSE 3000
EXPOSE 80/tcp
EXPOSE 3000 443
Documentation
Indique aux développeurs quel port l'écoute
⚠️ Ne publie PAS le port
Pour publier : docker run -p 3000:3000
CMD ["node", "server.js"]
# Forme exec (recommandée)
CMD node server.js
# Forme shell
Caractéristiques :
docker run# Au lancement :
docker run
mon-image npm run dev# Remplace CMD par "npm run dev"
ENTRYPOINT ["python"]
CMD ["app.py"]
# docker run mon-image → python app.py
# docker run mon-image script.py → python script.py
CMD seul
Remplaceable entièrement
ENTRYPOINT + CMD
CMD = arguments par défaut d'ENTRYPOINT
💡 ENTRYPOINT fixe l'exécutable, CMD fournit les arguments par défaut
ARG NODE_VERSION=20
ARG APP_ENV
ENV NODE_ENV=production
ENV PORT=3000
ARG — Build-time
Disponible pendant le build uniquement
docker build --build-arg APP_ENV=prod
ENV — Run-time
Persiste dans le conteneur final
docker run -e NODE_ENV=development
✅ NODE_ENV=production désactive les devDependencies dans npm install
Comment Docker optimise la construction
# Dockerfile
# Chaque instruction = 1 layer
FROM node:20-alpine # Layer 1: OS de base ≈ 135 Mo
WORKDIR /app # Layer 2: metadata du dossier
COPY package.json . # Layer 3: fichier ajouté
RUN npm install # Layer 4: node_modules ≈ 50 Mo
COPY . . # Layer 5: code source
Union Filesystem — Les layers s'empilent
Chaque layer = diff du précédent. Docker les assemble en lecture seule.
Comment ça marche :
🚀 Build avec cache : millisecondes
Build sans cache : plusieurs secondes
💡 docker build --no-cache pour forcer un rebuild complet
❌ Mauvais ordre
COPY . .
RUN npm install
Cache invalidé à chaque changement de code !
✅ Bon ordre
COPY package.json .
RUN npm install
COPY . .
npm install en cache tant que package.json ne change pas !
📦 Les dépendances changent beaucoup moins souvent que le code source
$ docker build -t mon-app .
✔ CACHED [1/5] FROM node:20-alpine
✔ CACHED [2/5] WORKDIR /app
✔ CACHED [3/5] COPY package.json .
✔ CACHED [4/5] RUN npm install
► BUILD [5/5] COPY . .
// Changement dans server.js → seuls les layers 5 est rebuildé
// Changement dans package.json → layers 3, 4, 5 sont rebuildés
🚀 4 layers en cache sur 5 → build en ~1 seconde au lieu de 30s
Dockeriser une app Node.js/Express
// package.json
{
"name": "mon-app",
"scripts": { "start": "node server.js" },
"dependencies": { "express": "^4.18" }
}
// server.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello Docker!');
});
app.listen(process.env.PORT || 3000);
FROM node:20
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
❌ Problèmes :
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json .
RUN npm ci --only=production
COPY . .
EXPOSE 3000
ENV NODE_ENV=production
CMD ["node", "server.js"]
✅ package.json AVANT le code source → cache préservé pour npm ci
Image finale ≈ 135 Mo au lieu de 1 Go
1er build :
$ docker build -t mon-app .
► BUILD [1/5] FROM node:20-alpine (téléchargement)
► BUILD [2/5] WORKDIR /app
► BUILD [3/5] COPY package.json .
► BUILD [4/5] RUN npm ci --only=production (2min)
► BUILD [5/5] COPY . .
2e build (après modif de server.js) :
$ docker build -t mon-app .
✔ CACHED [1/5] FROM node:20-alpine
✔ CACHED [2/5] WORKDIR /app
✔ CACHED [3/5] COPY package.json .
✔ CACHED [4/5] RUN npm ci
► BUILD [5/5] COPY . . (0.5s)
Images légères, sûres et rapides à builder
# .dockerignore
node_modules
.git
.env
.gitignore
dist/
*.md
Dockerfile
❌ Sans .dockerignore
node_modules copié dans l'image → conflits plateforme, image énorme
✅ Avec .dockerignore
Contexte de build allégé, build plus rapide, image plus petite
💡 .dockerignore fonctionne comme .gitignore
Les fichiers exclus ne sont même PAS envoyés au démon Docker
node:20
1 Go
Basé sur Debian complet
node:20-slim
~200 Mo
Debian minimal
node:20-alpine
~135 Mo
Alpine Linux (musl libc)
✅ alpine = 7× plus petit que l'image full !
Moins de surface d'attaque = plus sécurisé
❌ Root par défaut
FROM node:20-alpine
RUN npm install
CMD ["node", "app.js"]
Si l'app est compromise → attaquant = ROOT
✅ Utilisateur dédié
FROM node:20-alpine
RUN npm install
USER node
CMD ["node", "app.js"]
L'utilisateur node existe déjà dans l'image officielle
💡 Pour les images non-Node : créez votre propre utilisateur
RUN addgroup -S app && adduser -S app -G app
# Dockerfile optimisé 🚀
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json .
RUN npm ci --only=production && npm cache clean --force
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]
✅ 135 Mo | ✅ Cache optimisé | ✅ Non-root | ✅ Production
ENTRYPOINT ["python"]
CMD ["app.py"]
| Usage | Commande exécutée |
|---|---|
| docker run mon-image | python app.py |
| docker run mon-image script.py | python script.py |
| docker run mon-image --help | python --help |
💡 ENTRYPOINT = exécutable fixe | CMD = arguments par défaut
Les erreurs à éviter absolument
Copier tout le projet avant d'installer les dépendances
// ❌ Cache invalidé à chaque changement
COPY . .
RUN npm install # ← re-télécharge tout!
✅ Solution : dépendances d'abord, code ensuite
COPY package.json.
RUN npm install # ← en cache tant que package.json est identique
COPY . .
Les conséquences :
✅ Solution :
Toujours créer un .dockerignore AVANT d'écrire le Dockerfile
node:20
1 Go
GCC, Perl, curl, tout l'OS Debian
node:20-alpine
135 Mo
Juste l'essentiel (+ musl libc)
💡 Taille réduite = déploiements plus rapides, moins de surface d'attaque
Risque de sécurité majeur
Si un attaquant exploite une vulnérabilité dans l'application, il a les pleins droits sur le conteneur → escalade vers l'hôte possible
✅ Solution : USER node
L'image officielle Node fournit déjà l'utilisateur node
📝 Ordre des instructions
package.json AVANT le code source → cache préservé pour npm install
📦 Images minimales
Préférez alpine ou slim (135 Mo vs 1 Go)
🚫 .dockerignore
Exclure node_modules, .git, .env du contexte de build
🔒 USER non-root
Ne jamais laisser le processus tourner en root
⚡ CMD ≠ ENTRYPOINT
CMD est remplaçable, ENTRYPOINT est fixe (CMD devient ses arguments)
FROM
Image de base
WORKDIR
Répertoire de travail
COPY
Copier des fichiers
RUN
Exécuter des commandes
EXPOSE
Documenter le port
CMD/ENTRYPOINT
Commande de démarrage
⚡ Layers = cache = builds rapides
Ordonnez intelligemment vos instructions !
1. Dockerfile pour une API Express
Écrire un Dockerfile optimisé (alpine, non-root, .dockerignore)
2. Analyser les layers
docker history mon-image — observez chaque layer
3. Build avec et sans cache
Modifier server.js → rebuild. Puis modifier package.json → rebuild. Comparez!
4. Dockerfile multi-stage
Builder une app Node avec --only=dev, puis copier les artefacts dans une image alpine
Dockerfile : vous avez la recette !
FROM alpine
Build, Ship, Run — Vous maitrisez les Dockerfiles !