Une photo n'est pas la réalité. C'est son souvenir fixé dans l'argentique. Une image Docker n'est pas un conteneur. C'est son négatif. Son empreinte. Ce qui reste quand le corps a disparu.
3.1 Structure des Images - Les Couches de l'Histoire
Le Système de Couches : Comme un Palimpseste
Une image Docker, c'est une pile de calques.
Comme ces vieux dossiers où chaque page est un calque transparent.
Chaque couche = une modification.
Empilées. Immuables. En lecture seule.
La métaphore du manuscrit médiéval :
- Couche 1 : Parchemin vierge (l'image de base)
- Couche 2 : Texte en latin écrit au XIe siècle (RUN apt-get update)
- Couche 3 : Texte effacé, réécrit en français au XIVe (RUN apt-get install)
- Couche 4 : Enluminures ajoutées (COPY app)
- Couche supérieure : Notes au crayon qui s'effacent (container layer)
La magie ? Tous les manuscrits qui utilisent le même parchemin (Ubuntu 22.04) le partagent.
Pas besoin de 1000 parchemins identiques.
Gestion des Images : La Bibliothèque des Empreintes
# Télécharger une image (aller chercher le dossier aux archives)
docker pull nginx:alpine
# :latest si pas de tag, mais ne le fais pas. Sois précis.
# Voir ce qu'on a localement
docker images
# REPOSITORY TAG IMAGE ID CREATED SIZE
# nginx alpine abc123def456 2 weeks ago 23MB
# Taguer (donner un autre nom au même dossier)
docker tag nginx:alpine mon_serveur:v1
# Comme un alias. Une même image, deux noms.
# Pousser vers un registry (archiver dans la bibliothèque centrale)
docker push mon_serveur:v1
# Seules les couches absentes sont envoyées.
# Sauvegarder en fichier (le dossier complet)
docker save -o backup.tar mon_serveur:v1
# Utile pour le déplacement sans internet.
# Charger depuis un fichier (le dossier retrouvé)
docker load -i backup.tar
# Supprimer (brûler le dossier)
docker rmi mon_serveur:v1
# -f pour forcer si des conteneurs l'utilisent encore.
Les tags ne sont pas des versions.
Ils sont des pointeurs.
Comme les étiquettes sur les boîtes d'archives.
L'IMAGE ID : L'empreinte digitale
Chaque couche a un hash SHA256.
L'image ID est le hash de la dernière couche.
Unique. Immuable.
# Voir les couches d'une image
docker history nginx:alpine
# OUTPUT :
# IMAGE CREATED CREATED BY SIZE
# abc123def456 2 weeks ago /bin/sh -c #(nop) CMD ["nginx" "-g" "daemo… 0B
# def456ghi789 2 weeks ago /bin/sh -c #(nop) STOPSIGNAL SIGQUIT 0B
# ... etc ...
# Inspecter une image
docker inspect nginx:alpine | jq '.[0].RootFS.Layers'
# Liste les hashes de toutes les couches
3.2 Dockerfile : Le Script du Crime
Un Dockerfile, c'est la recette.
La liste des instructions pour créer l'empreinte.
Chaque ligne = une couche.
Sauf les métadonnées.
Structure Complète : L'Autopsie
# Commentaire. Inutile. Comme la plupart des commentaires.
# Sauf celui-ci.
# FROM : La base. Obligatoire. Première ligne.
# Comme choisir le cadavre sur lequel on va travailler.
FROM ubuntu:22.04
# LABEL : Les étiquettes. Pour la documentation.
# Comme les notes sur le bord du plan.
LABEL maintainer="contact@ombre.net"
LABEL version="1.0"
LABEL description="Une image qui ne sert à rien"
# ARG : Variable de build. N'existe qu'à la construction.
# Comme un post-it qu'on jette après.
ARG VERSION=1.0
ARG BUILD_DATE
# ENV : Variable d'environnement. Persiste dans le conteneur.
# Comme un tatouage sur le corps.
ENV APP_HOME=/app
ENV NODE_ENV=production
# WORKDIR : Le répertoire de travail. Créé si inexistant.
# La cellule où on travaille.
WORKDIR ${APP_HOME}
# COPY : Copier des fichiers du contexte de build.
# Apporter des pièces à conviction.
COPY package.json .
COPY src/ ./src/
# ADD : COPY avec super-pouvoirs (URL, extraction auto).
# Mais dangereux. Comme un couteau automatique.
# Préfère COPY.
# ADD https://example.com/file.tar.gz /tmp/
# RUN : Exécuter une commande pendant le build.
# La modification permanente.
RUN apt-get update && apt-get install -y \
python3 \
&& rm -rf /var/lib/apt/lists/*
# EXPOSE : Documentation du port. Ne fait rien.
# Comme dire "il y a une fenêtre ici".
EXPOSE 8080
# USER : Changer d'utilisateur. Pour la sécurité.
# Laisser l'arme à un civil, pas à un soldat.
USER nobody:nogroup
# VOLUME : Déclarer un point de montage.
# La porte vers l'extérieur.
VOLUME ["/data"]
# CMD : Commande par défaut au démarrage.
# Peut être écrasée par docker run.
CMD ["python3", "app.py"]
# ENTRYPOINT : Le binaire exécuté.
# Le programme qui tourne dans la cellule.
# CMD devient ses arguments.
ENTRYPOINT ["python3"]
# Avec CMD ["app.py"]
CMD vs ENTRYPOINT : Le Dilemme du Bourreau
CMD seul : La commande par défaut, remplaçable.
CMD ["nginx", "-g", "daemon off;"]
# docker run mon_image → nginx -g daemon off;
# docker run mon_image sh → remplace CMD par sh
ENTRYPOINT seul : L'exécutable fixe.
ENTRYPOINT ["nginx"]
# docker run mon_image -g "daemon off;" → nginx -g "daemon off;"
# Toujours nginx, on ne peut pas changer.
Les deux : Le combo classique.
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
# docker run mon_image → nginx -g "daemon off;"
# docker run mon_image -t → nginx -t (test config)
Shell vs Exec form :
# Shell form (pas de tableau) - exécuté via /bin/sh -c
CMD nginx -g "daemon off;"
# Exec form (tableau) - exécuté directement
CMD ["nginx", "-g", "daemon off;"]
# Préfère exec form. Pas d'interpréteur shell. Plus propre.
Bonnes Pratiques : Le Code du Meurtrier Propre
1. Images officielles et tags spécifiques
# MAUVAIS
FROM ubuntu
# Quelle version ? latest change.
# BON
FROM ubuntu:22.04
# Ou mieux :
FROM ubuntu:22.04@sha256:abc123...
# Le hash est immuable.
2. Minimiser les couches (combiner les RUN)
# MAUVAIS (3 couches)
RUN apt-get update
RUN apt-get install -y python3
RUN rm -rf /var/lib/apt/lists/*
# BON (1 couche)
RUN apt-get update && \
apt-get install -y python3 && \
rm -rf /var/lib/apt/lists/*
3. Ordonner du plus stable au moins stable
# L'ordre compte pour le cache.
COPY package.json . # Change peu
RUN npm install # Lent, cache si package.json identique
COPY . . # Change souvent
4. Nettoyer après soi
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
5. Utilisateurs non-privilégiés
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Pas root. Jamais root.
6. .dockerignore : Ce qu'on ne copie pas
# Comme .gitignore pour Docker
.git
node_modules
*.log
Dockerfile
README.md
3.3 Builds Multi-étapes : Le Crime en Deux Actes
Le Concept : Un Atelier, une Galerie
Problème : Tu as besoin d'un compilateur pour construire ton app.
Mais le compilateur pèse 500 Mo. Ton app, 5 Mo.
Tu ne veux pas expédier le compilateur avec l'app.
Solution : Deux pièces.
- Pièce 1 (l'atelier) : Le compilateur, les outils, le bruit, la sciure.
- Pièce 2 (la galerie) : Juste l'œuvre finie. Propre. Minimaliste.
Tu construis dans la pièce 1, tu exposes dans la pièce 2.
Exemple Complet : Une App Node.js
# ---------- ÉTAPE 1 : L'ATELIER ----------
# Ici on a tout. Node, npm, les dev dependencies.
FROM node:18 AS builder
# AS donne un nom à cette étape
WORKDIR /app
# 1. Copier seulement package.json d'abord (pour le cache)
COPY package*.json ./
# 2. Installer les dépendances (développement incluses)
RUN npm ci --only=production
# ci est plus fiable que install pour les builds
# 3. Copier le reste du code
COPY . .
# 4. Builder l'application (transpile, minifie, etc.)
RUN npm run build
# Maintenant on a un dossier /app/dist prêt
# ---------- ÉTAPE 2 : LA GALERIE ----------
# Ici on veut le minimum. Juste servir des fichiers statiques.
FROM nginx:alpine AS runner
# Alpine = léger (5 Mo vs 100 Mo pour nginx normal)
# On est dans /usr/share/nginx/html par défaut avec nginx
# Copier DEPUIS le builder, pas depuis le host
COPY --from=builder /app/dist /usr/share/nginx/html
# --from=builder : va chercher dans l'image nommée "builder"
# /app/dist : le chemin dans le builder
# /usr/share/nginx/html : le chemin dans le runner
# Exposer le port (documentation)
EXPOSE 80
# Nginx démarre automatiquement, pas besoin de CMD
# Mais si on veut :
# CMD ["nginx", "-g", "daemon off;"]
Résultat :
- Image builder : ~1 Go (Node + tout)
- Image runner : ~25 Mo (nginx + fichiers statiques)
- 97.5% de poids en moins
Autre Exemple : Compilation C++
# Étape de build (avec tous les outils)
FROM gcc:12 AS compiler
WORKDIR /src
COPY hello.c .
RUN gcc -o hello hello.c -static
# -static : tout dans le binaire, pas de dépendances externes
# Étape d'exécution (scratch = vide)
FROM scratch
# Le plus minimal possible. Même pas de shell.
COPY --from=compiler /src/hello /hello
# Copier SEULEMENT le binaire
CMD ["/hello"]
# Exécuter le binaire directement
Scratch : L'image vide.
Le néant.
Parfait pour un binaire statique.
Quelques kilo-octets seulement.
Avancé : BuildKit et Secrets
BuildKit : Le nouveau moteur de build (plus rapide, plus sûr).
# Activer
DOCKER_BUILDKIT=1 docker build -t mon_image .
# Ou configurer dans /etc/docker/daemon.json
# Avec secrets (ne pas mettre les mots de passe dans l'image)
docker build --secret id=mysecret,src=./secret.txt -t mon_image .
Dans le Dockerfile :
# syntax=docker/dockerfile:1
FROM alpine
# Lire le secret pendant le build
RUN --mount=type=secret,id=mysecret \
export API_KEY=$(cat /run/secrets/mysecret) && \
echo "Building with key..."
# Le secret n'apparaît pas dans les couches finales
Scène de Crime : Création d'une Image Complète
# 1. Préparer le contexte
mkdir mon_app && cd mon_app
# 2. Écrire l'app minimale
cat > app.py << 'EOF'
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return "La ville dort, mais l'image veille."
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)
EOF
# 3. Écrire le Dockerfile
cat > Dockerfile << 'EOF'
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder
WORKDIR /app
# Installer les dépendances
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Image finale
FROM python:3.11-slim
WORKDIR /app
# Copier les dépendances depuis le builder
COPY --from=builder /root/.local /root/.local
# Copier l'application
COPY app.py .
# Utilisateur non-root
RUN useradd -m -u 1000 appuser
USER appuser
# Variables d'environnement
ENV PATH=/root/.local/bin:$PATH \
FLASK_APP=app.py
# Exposer le port
EXPOSE 8080
# Commande
CMD ["flask", "run", "--host=0.0.0.0", "--port=8080"]
EOF
# 4. Fichier requirements
echo "Flask==2.3.3" > requirements.txt
# 5. .dockerignore
cat > .dockerignore << 'EOF'
__pycache__
*.pyc
*.pyo
.env
.git
EOF
# 6. Build
docker build -t mon_app:v1 .
# 7. Vérifier
docker images | grep mon_app
docker history mon_app:v1
# 8. Lancer
docker run -d -p 8080:8080 --name app_monitoring mon_app:v1
curl localhost:8080
Conclusion du Module
Une image Docker, c'est plus qu'un fichier.
C'est une promesse.
La promesse que ce qui a marché sur ta machine marchera ailleurs.
La promesse que l'environnement est reproductible.
Les règles du mouleur d'images :
- Couches minimales - Chaque RUN est une couche. Combine.
- Ordre stratégique - Ce qui change peu en haut, ce qui change souvent en bas.
- Multi-stage - L'atelier et la galerie. Ne montre que le résultat.
- Non-root - L'utilisateur le moins privilégié.
- Tags précis - Pas de :latest. Des versions, des hashs.
Maintenant tu sais créer les moules.
Les négatifs.
Les empreintes.
Prochain module : Docker Compose.
Quand les conteneurs doivent travailler ensemble.
Comme une équipe de braquage.
Chacun son rôle, mais un but commun.