Maitriser la syntaxe Dockerfile

FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT

Construisez vos propres images Docker optimisées

Objectifs de la leçon

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

Plan du cours

1

Rappel : images vs conteneurs

docker run, aujourd'hui on crée nos propres images

2

Le Dockerfile

Un fichier texte instruction par instruction

3

Les instructions clés

FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT, ARG, ENV

4

Layers et cache

Chaque instruction crée un layer, le cache accélère les rebuilds

5

Live coding : app Node.js/Express

Impact de l'ordre des instructions sur le cache

6

Bonnes pratiques

.dockerignore, images minimales, utilisateur non-root

1. Rappel : Images vs Conteneurs

Les bases avant de construire

Image vs Conteneur

📦 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...)

2. Le Dockerfile

La recette de votre image

Un Dockerfile, c'est quoi ?

# 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

3. Les instructions clés

FROM, RUN, COPY, WORKDIR, EXPOSE, CMD, ENTRYPOINT

FROM — L'image de base

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 — Le répertoire de travail

WORKDIR /app

✅ Ce que ça fait :

  • Crée le dossier s'il n'existe pas
  • Toutes les instructions suivantes s'exécutent depuis ce dossier
  • Remplace les cd à répétition

⚠️ Sans WORKDIR, on est à la racine (/)

COPY — Ajouter des fichiers

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 — Exécuter des commandes

RUN npm install

RUN apt-get update && apt-get install -y curl

RUN npm run build

Bonnes pratiques :

  • Chaînez les commandes avec &&
  • Nettoyez les caches dans la même RUN
  • 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 — Documenter le port

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 — La commande par défaut

CMD ["node", "server.js"]

# Forme exec (recommandée)

CMD node server.js

# Forme shell

Caractéristiques :

  • Peut être remplacée au docker run
  • Un seul CMD par Dockerfile (le dernier l'emporte)

# Au lancement :

docker run

mon-image npm run dev

# Remplace CMD par "npm run dev"

ENTRYPOINT — Le point d'entrée fixe

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 & ENV — Variables

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

4. Le système de layers

Comment Docker optimise la construction

Chaque instruction = un layer

# 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.

Le cache des layers

Comment ça marche :

  • Si un layer n'a pas changé → Docker le réutilise
  • Si un layer change → tous les layers suivants sont rebuildés
  • Docker compare le hash du contenu pour COPY et les commandes pour RUN

🚀 Build avec cache : millisecondes

Build sans cache : plusieurs secondes

💡 docker build --no-cache pour forcer un rebuild complet

L'ordre est crucial

❌ 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

Visualisation du build

$ 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

5. Live Coding

Dockeriser une app Node.js/Express

Notre app 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);

Dockerfile — Version naïve

FROM node:20

COPY . .

RUN npm install

EXPOSE 3000

CMD ["node", "server.js"]

❌ Problèmes :

  • COPY . . avant npm install → cache invalidé à chaque changement
  • Image node:20 = 1 Go !
  • Aucun .dockerignore
  • Process tourne en root

Dockerfile — Version optimisée

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

Démonstration du cache en direct

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)

6. Bonnes pratiques

Images légères, sûres et rapides à builder

.dockerignore — Le garde-barrière

# .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

Choisir une image de base légère

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é

Sécurité : Utilisateur non-root

❌ 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 final — Tout ensemble

# 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

CMD vs ENTRYPOINT — Récapitulatif

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

⚠️ Pièges courants

Les erreurs à éviter absolument

❌ Piège n°1 : Ordre des instructions

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 . .

❌ Piège n°2 : Oubli du .dockerignore

Les conséquences :

  • 🔴 node_modules de l'hôte copié dans l'image
  • 🔴 Conflits de plateforme (macOS ≠ Linux)
  • 🔴 Image inutilement lourde (centaines de Mo ajoutés)
  • 🔴 .env exposé dans l'image → fuite de secrets!

✅ Solution :

Toujours créer un .dockerignore AVANT d'écrire le Dockerfile

❌ Piège n°3 : Image de base trop lourde

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

❌ Piège n°4 : Processus en root

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

Points clés à retenir

📝 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)

Récapitulatif

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 !

Exercices pratiques

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

Questions ?

Dockerfile : vous avez la recette !

FROM alpine

Build, Ship, Run — Vous maitrisez les Dockerfiles !