Skip to content

Module 1 — Docker avancé : builds multi-étapes et non-root

Bonne lecture et bon apprentissage ! Junior TSAFACK – 12/09/2026 Temps de lecture estimé : 9 minutes


Ce module suppose une familiarité de base avec Docker (déjà vue dans le cours d’installation de Podman, l’équivalent rootless de Docker). L’accent est mis ici sur deux pratiques qui séparent une image « qui marche » d’une image « prête pour la production » : le build multi-étapes et l’utilisateur non-root.

Une petite API Flask, avec un compteur de visites stocké dans Redis (pour démontrer, au module suivant, la communication entre conteneurs) :

app.py
import os
import redis
from flask import Flask, jsonify
app = Flask(__name__)
r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379, decode_responses=True)
@app.route("/")
def index():
count = r.incr("visites")
return jsonify(message="Bonjour depuis le conteneur applicatif", visites=count)
@app.route("/health")
def health():
return jsonify(status="ok")
Dockerfile.naive
FROM python:3.12
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]
Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
WORKDIR /app
RUN groupadd -r appuser && useradd -r -g appuser appuser
COPY --from=builder /install /usr/local
COPY app.py .
USER appuser
EXPOSE 5000
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

Ce que le multi-stage change concrètement : la première étape (builder) installe les dépendances avec tous les outils de compilation nécessaires (souvent volumineux) ; la seconde étape repart d’une image propre et ne récupère, via COPY --from=builder, que le résultat de l’installation (/usr/local) — pas les outils de build eux-mêmes. Le conteneur final ne contient jamais les compilateurs, en-têtes de développement, ou caches de build utilisés pour le fabriquer.

Les deux images, construites et comparées réellement

Section titled “Les deux images, construites et comparées réellement”
$ docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}"
docker-avance-app-naive:latest 1.63GB
docker-avance-app:latest 202MB

Un facteur 8 sur la taille, avec exactement le même code applicatif. Une image plus petite, c’est un déploiement plus rapide (moins à télécharger sur chaque nœud), une surface d’attaque réduite (moins de binaires présents = moins de vulnérabilités potentielles), et des scans de sécurité (voir le module Sécurité & Conformité) plus rapides et plus faciles à trier.

$ docker run --rm docker-avance-app-naive whoami
root
$ docker run --rm docker-avance-app whoami
appuser

Pourquoi c’est important, concrètement : un conteneur qui tourne en root et qui serait compromis (par exemple via une vulnérabilité de la bibliothèque flask ou d’une de ses dépendances) donne à l’attaquant les pleins pouvoirs à l’intérieur du conteneur — et selon la configuration du moteur de conteneurs, potentiellement un chemin d’escalade vers l’hôte. Faire tourner le processus applicatif en tant qu’utilisateur non privilégié (USER appuser) limite ce que cette compromission peut accomplir, même si le conteneur lui-même reste vulnérable — c’est le principe du moindre privilège, appliqué au niveau du conteneur.

Orchestrer deux conteneurs avec Docker Compose

Section titled “Orchestrer deux conteneurs avec Docker Compose”
docker-compose.yml
services:
app:
build: .
ports:
- "5001:5000"
environment:
- REDIS_HOST=redis
depends_on:
- redis
redis:
image: redis:7-alpine
restart: unless-stopped

Démarré réellement :

$ docker compose up -d
Container docker-avance-redis-1 Started
Container docker-avance-app-1 Started
$ docker compose ps
NAME STATUS PORTS
docker-avance-app-1 Up 3 seconds 0.0.0.0:5001->5000/tcp
docker-avance-redis-1 Up 4 seconds
$ curl -s http://localhost:5001/
{"message":"Bonjour depuis le conteneur applicatif","visites":1}
$ curl -s http://localhost:5001/
{"message":"Bonjour depuis le conteneur applicatif","visites":2}
$ curl -s http://localhost:5001/
{"message":"Bonjour depuis le conteneur applicatif","visites":3}

Le compteur s’incrémente correctement à chaque requête : les deux conteneurs communiquent bien via le réseau Docker créé automatiquement par Compose (l’application résout redis — le nom du service, pas une adresse IP codée en dur — grâce au DNS interne de ce réseau).

Module 2 : Kubernetes — déployer une vraie application sur un cluster réel