Polar Code 🎭

Command Palette

Search for a command to run...

03
Pièce N°03

Module 3 : Création et Gestion des Images - Les Moules et les Masques

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 :

  1. Couches minimales - Chaque RUN est une couche. Combine.
  2. Ordre stratégique - Ce qui change peu en haut, ce qui change souvent en bas.
  3. Multi-stage - L'atelier et la galerie. Ne montre que le résultat.
  4. Non-root - L'utilisateur le moins privilégié.
  5. 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.