Exercices Docker — Partie 1 Exercices 1 à 5 — Réponses Loïs Beaujard — YNOV B3 — 2026
Aide de L'IA pour la formulation des réponses
Objectif : Comprendre le cycle de vie d'un conteneur et les commandes fondamentales.
1.1 — Télécharger l'image nginx:alpine Commande : docker pull nginx:alpine Cette commande télécharge l'image depuis Docker Hub sans lancer de conteneur.
1.2 — Lancer le conteneur en arrière-plan Commande : docker run -d --name mon-nginx -p 8080:80 nginx:alpine -d : mode detached (arrière-plan) --name mon-nginx : nom du conteneur -p 8080:80 : redirige le port 8080 de la machine vers le port 80 du conteneur
1.3 — Lister les conteneurs en cours d'exécution Commande : docker ps Affiche uniquement les conteneurs dont le statut est actif (Running).
1.4 — Vérifier dans le navigateur ou avec curl Commande : curl http://localhost:8080 On voit la page d'accueil Nginx "Welcome to nginx!". Le conteneur tourne et répond sur le port 8080.
1.5 — Afficher les logs du conteneur Commande : docker logs mon-nginx Les logs montrent le démarrage de Nginx et les requêtes HTTP reçues (GET /, codes 200, 404 pour favicon.ico, etc.).
1.6 — Arrêter et lister tous les conteneurs Commandes : docker stop mon-nginx / docker ps -a Différence :
- docker ps : affiche uniquement les conteneurs actifs (status = Up)
- docker ps -a : affiche TOUS les conteneurs, y compris arrêtés (status = Exited) Après le stop, mon-nginx apparaît avec le statut "Exited (0)" dans docker ps -a, mais disparaît de docker ps seul.
1.7 — Supprimer le conteneur et vérifier Commandes : docker rm mon-nginx / docker ps -a Après suppression, le conteneur n'apparaît plus dans la liste, même avec le flag -a.
1.8 — Suppression automatique à l'arrêt Commande : docker run -d --name mon-nginx -p 8080:80 --rm nginx:alpine Le flag --rm supprime automatiquement le conteneur dès qu'il s'arrête.
======================================== EXERCICE 2 — Construire sa première image avec un Dockerfile
Objectif : Écrire un Dockerfile, construire une image, comprendre les layers.
2.1 — Fichier index.html
<title>Ma première image Docker</title>Lois
2.2 — Dockerfile FROM nginx:alpine COPY index.html /usr/share/nginx/html/index.html EXPOSE 80 FROM : image de base. COPY : copie le HTML dans le dossier servi par Nginx. EXPOSE : documente le port d'écoute.
2.3 — Construire l'image avec le tag mon-site:v1 Commande : docker build -t mon-site:v1 . Le point (.) indique que le Dockerfile est dans le dossier courant. -t permet de nommer et tagger l'image.
2.4 — Lancer le conteneur sur le port 9090 Commande : docker run -d -p 9090:80 --rm mon-site:v1 En ouvrant http://localhost:9090, on voit la page HTML avec le titre et le prénom.
2.5 — Taille des images Commande : docker images mon-site:v1 = 92.6 MB / nginx:alpine = 93.5 MB Quasi identiques : on a seulement ajouté un fichier HTML de 24.6 kB.
2.6 — Layers de l'image Commande : docker history mon-site:v1 nginx:alpine possède 20 layers existants. Notre build en ajoute 2 :
- COPY index.html (24.6 kB)
- EXPOSE 80 (0 B, instruction documentaire) Les layers nginx:alpine apparaissent comme car ils viennent d'une image externe.
2.7 — Modification et reconstruction en v2 Commande : docker build -t mon-site:v2 . Étape depuis le cache : CACHED [1/2] FROM docker.io/library/nginx:alpine (image de base inchangée) Étape réexécutée : [2/2] COPY index.html (fichier modifié) Principe : tout ce qui est avant la modification est mis en cache, tout ce qui est après est réexécuté.
2.8 — Supprimer mon-site:v1 Commandes : docker stop / docker rmi mon-site:v1 mon-site:v2 est conservée. Si un conteneur utilise encore l'image, Docker refuse avec "conflict: unable to delete".
Objectif : Comprendre la différence entre bind mount et volume nommé.
- Bind mount : Monte un dossier de l'hôte directement dans le conteneur
- Volume nommé : Géré par Docker, stocké dans /var/lib/docker/volumes/
3.1 — Le système de fichiers d'un conteneur est éphémère Commandes : docker run -it --rm alpine -> mkdir /data && echo "bonjour" > /data/test.txt -> exit docker run -it --rm alpine -> cat /data/test.txt Résultat : cat: can't open '/data/test.txt': No such file or directory Le fichier n'existe plus. Chaque conteneur a son propre système de fichiers isolé. Quand il est supprimé (--rm), toutes les données écrites dedans sont perdues.
3.2 — Bind mount Commande : docker run -d -p 8080:80 --rm -v "${PWD}/html:/usr/share/nginx/html" nginx:alpine En modifiant index.html sur la machine hôte sans redémarrer, le changement est visible instantanément dans le navigateur. Le dossier est monté directement, Nginx lit les fichiers en temps réel.
3.3 — Créer un volume nommé Commande : docker volume create mes-donnees
3.4 — Écrire dans le volume Commandes : docker run -it --rm -v mes-donnees:/data alpine -> echo "je survis" > /data/persistant.txt -> exit Le fichier est écrit dans le volume, pas dans le conteneur. Le volume survit à la suppression du conteneur.
3.5 — Vérifier la persistance sur un nouveau conteneur Commandes : docker run -it --rm -v mes-donnees:/data alpine -> cat /data/persistant.txt Résultat : "je survis" Le fichier existe dans ce nouveau conteneur. Les données sont stockées dans le volume, indépendamment du cycle de vie des conteneurs. Cas d'usage typique : base de données (MySQL, PostgreSQL).
3.6 — Lister les volumes et leur emplacement physique Commandes : docker volume ls / docker volume inspect mes-donnees Résultat : "Mountpoint": "/var/lib/docker/volumes/mes-donnees/_data" Sur Windows avec Docker Desktop, ce chemin est dans la VM Linux WSL2, pas directement accessible depuis l'Explorateur.
3.7 — Supprimer le volume Commande : docker volume rm mes-donnees Précaution : s'assurer qu'aucun conteneur n'utilise ce volume. La suppression est irréversible, toutes les données sont perdues définitivement.
Objectif : Comprendre les réseaux Docker, faire communiquer deux conteneurs par leur nom.
Contexte : Par défaut, les conteneurs sont connectés au réseau bridge. Sur un réseau bridge personnalisé, les conteneurs peuvent se joindre par leur nom (DNS automatique). Ce n'est pas le cas sur le réseau bridge par défaut.
4.1 — Lister les réseaux Docker existants Commande : docker network ls
Les trois réseaux créés par défaut :
- bridge : réseau par défaut pour les conteneurs, driver bridge
- host : partage le réseau de la machine hôte directement
- none : aucun réseau, le conteneur est complètement isolé
4.2 — Créer un réseau bridge personnalisé Commande : docker network create mon-reseau
4.3 — Lancer serveur-web connecté à mon-reseau Commande : docker run -d --name serveur-web --network mon-reseau nginx:alpine
4.4 — Lancer client et tester la communication par nom Commande : docker run -it --name client --network mon-reseau alpine Dans le conteneur : wget -qO- http://serveur-web
Résultat : on récupère le HTML de la page d'accueil Nginx (Welcome to nginx!).
Pourquoi peut-on utiliser le nom "serveur-web" plutôt qu'une IP ? Sur un réseau bridge personnalisé, Docker intègre un serveur DNS automatique. Chaque conteneur est enregistré sous son nom dans ce DNS. Ainsi "serveur-web" est résolu automatiquement en l'adresse IP du conteneur correspondant. Ce mécanisme n'existe pas sur le réseau bridge par défaut.
4.5 — Tester depuis client-externe (réseau par défaut) Commande : docker run -it --name client-externe alpine Dans le conteneur : wget -qO- http://serveur-web
Résultat : wget: bad address 'serveur-web'
Pourquoi ? client-externe est connecté au réseau bridge par défaut, pas à mon-reseau. Le DNS automatique de mon-reseau ne lui est pas accessible. Docker ne résout pas les noms de conteneurs sur le réseau bridge par défaut. Les deux réseaux sont isolés l'un de l'autre.
4.6 — Connecter client-externe à mon-reseau après démarrage Commande : docker network connect mon-reseau client-externe Puis : docker start -ai client-externe -> wget -qO- http://serveur-web
Résultat : la page Nginx s'affiche. Une fois connecté à mon-reseau, client-externe bénéficie du DNS automatique et peut joindre serveur-web par son nom.
4.7 — Nettoyage Commandes : docker stop serveur-web client-externe docker rm serveur-web client client-externe docker network rm mon-reseau
Objectif : Containeriser une application Python Flask, gérer les dépendances, configurer via des variables d'environnement.
5.1 — app.py from flask import Flask import os
app = Flask(name)
@app.route("/") def home(): env = os.environ.get("APP_ENV", "développement") return f"
Environnement : {env}
"@app.route("/health") def health(): return {"status": "ok"}, 200
if name == "main": app.run(host="0.0.0.0", port=5000)
5.2 — requirements.txt Flask==3.0.3
5.3 — Dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD ["flask", "run", "--host=0.0.0.0"]
Pourquoi cet ordre est-il important pour le cache Docker ? On copie requirements.txt seul AVANT de copier le reste du code. Ainsi, Docker met en cache le layer d'installation des dépendances (pip install). Si on modifie uniquement app.py, Docker réutilise le cache du pip install et ne réinstalle pas les dépendances. Si on avait copié tous les fichiers d'un coup (COPY . .), n'importe quelle modification de app.py invaliderait le cache et forcerait une réinstallation complète des dépendances à chaque build.
5.4 — Construire l'image flask-app:v1 Commande : docker build -t flask-app:v1 . Build réussi en 11.6s, 5 steps dont le pip install des dépendances Flask.
5.5 — Lancer avec APP_ENV=production Commande : docker run -d -p 8888:5000 -e APP_ENV=production --name flask-prod flask-app:v1
- http://localhost:8888 affiche : "Flask fonctionne ! Environnement : production"
- http://localhost:8888/health retourne : {"status": "ok"} avec code 200
5.6 — Lancer sans APP_ENV Commande : docker run -d -p 8889:5000 --name flask-dev flask-app:v1 La valeur affichée est : "développement" Elle vient de la valeur par défaut définie dans le code Python : os.environ.get("APP_ENV", "développement") Quand la variable d'environnement APP_ENV n'est pas passée au conteneur, Python utilise la valeur de fallback "développement".
5.7 — Taille de l'image et pistes de réduction Commande : docker images flask-app:v1 Taille : 197 MB (content size : 48.1 MB)
Deux pistes pour réduire davantage la taille :
- Utiliser un multi-stage build : builder dans une image complète, puis copier uniquement les fichiers nécessaires dans une image finale ultra-légère (ex: python:3.12-alpine ~50MB au lieu de slim ~130MB).
- Utiliser python:3.12-alpine comme image de base à la place de python:3.12-slim. Alpine est beaucoup plus légère (~5MB de base vs ~75MB pour slim) mais nécessite parfois de compiler certaines dépendances C.
Objectif : Orchestrer plusieurs services avec Docker Compose V2, comprendre depends_on, les réseaux et volumes déclaratifs.
6.1 — app/app.py from flask import Flask import redis import os
app = Flask(name) r = redis.Redis(host=os.environ.get("REDIS_HOST", "redis"), port=6379)
@app.route("/") def home(): count = r.incr("visites") return f"
"@app.route("/reset") def reset(): r.set("visites", 0) return "Compteur remis à zéro", 200
6.2 — app/requirements.txt flask==3.0.3 redis==5.0.8
6.3 — app/Dockerfile FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . EXPOSE 5000 CMD ["flask", "run", "--host=0.0.0.0"]
6.4 — compose.yaml services: web: build: ./app ports: - "5000:5000" environment: - REDIS_HOST=redis depends_on: - redis
redis: image: redis:7-alpine volumes: - redis-data:/data
volumes: redis-data:
6.5 — Démarrer la stack en arrière-plan Commande : docker compose up -d Docker Compose V2 (sans tiret) démarre tous les services définis dans compose.yaml en arrière-plan. Il crée automatiquement le réseau, le volume et build l'image web si nécessaire.
6.6 — Test du compteur En visitant http://localhost:5000 plusieurs fois, le compteur s'incrémente à chaque visite (Visiteur n°1, n°2, n°3...). En visitant /reset, le compteur est remis à 0. La prochaine visite sur / repart de 1.
6.7 — Persistance après down/up Commandes : docker compose down / docker compose up -d Le compteur ne repart PAS de zéro. Il reprend là où il s'était arrêté. Pourquoi ? Le volume nommé redis-data persiste les données Redis sur le disque. Quand docker compose down est exécuté, les conteneurs sont supprimés mais le volume redis-data reste. Au redémarrage, Redis recharge ses données depuis ce volume (on voit dans les logs : "Loading RDB" / "keys loaded: 1"). Sans ce volume, Redis repartirait de zéro à chaque redémarrage.
6.8 — Logs en temps réel de tous les services Commande : docker compose logs -f Le flag -f (follow) affiche les logs en continu de tous les services (web et redis) avec le nom du service en préfixe.
6.9 — Shell interactif dans le conteneur web via Compose Commande : docker compose exec web sh Cette commande ouvre un shell dans le service "web" sans avoir besoin de connaître l'ID ou le nom exact du conteneur. Compose résout lui-même le conteneur correspondant au service.
6.10 — Arrêt complet avec suppression des volumes Commande : docker compose down -v Sans -v : supprime les conteneurs et le réseau, mais conserve le volume redis-data. Avec -v : supprime également tous les volumes déclarés dans compose.yaml (ici redis-data). Les données Redis sont définitivement perdues.
======================================== EXERCICE 7 — Variables d'environnement, fichiers .env et surcharge
Objectif : Gérer la configuration d'une stack Compose via .env, comprendre la surcharge et les bonnes pratiques de sécurité.
7.1 — .env APP_PORT=8080 APP_ENV=development POSTGRES_USER=admin POSTGRES_PASSWORD=secret123 POSTGRES_DB=myapp
7.2 — .gitignore .env
Pourquoi est-ce important ? Le fichier .env contient des secrets (mots de passe, clés API...). Le committer dans Git l'exposerait à toute personne ayant accès au dépôt, y compris dans l'historique Git même après suppression. C'est une faille de sécurité majeure. On l'exclut via .gitignore pour ne jamais le pousser sur GitHub/GitLab.
7.3 — .env.example APP_PORT=8080 APP_ENV=development POSTGRES_USER=admin POSTGRES_PASSWORD=changeme POSTGRES_DB=myapp
Rôle dans un projet d'équipe : .env.example est la version "template" du .env, sans les vraies valeurs sensibles. Il est commité dans Git. Quand un nouveau développeur clone le projet, il copie .env.example en .env et remplace les placeholders par ses propres valeurs. C'est la convention standard pour documenter les variables d'environnement nécessaires sans exposer les secrets.
7.4 — compose.yaml services: app: image: nginx:alpine ports: - "${APP_PORT}:80" environment: - APP_ENV=${APP_ENV}
db: image: postgres:16-alpine environment: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB}
7.5 — Lancer la stack Commande : docker compose up -d Nginx est accessible sur http://localhost:8080 (valeur de APP_PORT dans .env).
7.6 — Afficher le Compose après interpolation des variables Commande : docker compose config Cette commande affiche le fichier Compose avec toutes les variables ${...} remplacées par leurs valeurs réelles. Très utile pour déboguer et vérifier que les variables sont bien injectées. Exemple de résultat : published: "8080", POSTGRES_PASSWORD: secret123, etc.
7.7 — Surcharger APP_PORT depuis le shell sans modifier .env Commande PowerShell : $env:APP_PORT=9999; docker compose up -d Nginx sera accessible sur le port 9999. La variable définie dans le shell a une priorité plus haute que celle du fichier .env. Docker Compose utilise 9999 même si .env contient APP_PORT=8080.
7.8 — Priorité des variables Après modification de APP_PORT=9090 dans .env et relance avec docker compose up -d : Ordre de priorité (du plus fort au plus faible) :
- Variable définie directement dans le shell (ex: $env:APP_PORT=9999) — priorité maximale
- Variable définie dans le fichier .env — priorité intermédiaire
- Valeur définie directement dans compose.yaml (ex: APP_PORT: 8080) — priorité minimale => La variable shell écrase toujours .env, qui écrase toujours compose.yaml.
7.9 — Méthodes plus sécurisées pour les secrets en production
- Docker Secrets : mécanisme natif Docker Swarm qui monte les secrets comme des fichiers dans /run/secrets/ à l'intérieur du conteneur. Les secrets ne sont jamais exposés en clair dans les variables d'environnement ni dans docker inspect.
- Gestionnaire de secrets externe : utiliser un outil dédié comme HashiCorp Vault, AWS Secrets Manager, ou Azure Key Vault. L'application récupère les secrets au démarrage via une API sécurisée, chiffrée et avec contrôle d'accès fin. Les secrets ne transitent jamais en clair dans les fichiers de configuration.
======================================== EXERCICE 8 — Optimisation d'image : multi-stage build, .dockerignore et non-root
8.1 — Version naïve Dockerfile.naive : FROM python:3.12 WORKDIR /app COPY . . RUN pip install -r requirements.txt CMD ["python", "app.py"]
Commande : docker build -f Dockerfile.naive -t app-naive:v1 . Taille obtenue : 1.62 GB
8.2 — .dockerignore tests/ *.md pycache/ *.pyc .git .env
Pourquoi exclure tests/ du contexte de build de l'image de production ? Les tests ne sont pas nécessaires au fonctionnement de l'application en production. Les inclure augmente inutilement la taille de l'image et le contexte envoyé au daemon Docker. De plus, c'est une bonne pratique de sécurité : les fichiers de test peuvent contenir des données sensibles (fixtures, mocks avec des credentials...) qui n'ont rien à faire dans une image de production.
8.3 — Dockerfile multi-stage optimisé FROM python:3.12-slim AS builder WORKDIR /app COPY requirements.txt . RUN pip install --prefix=/install --no-cache-dir -r requirements.txt
FROM python:3.12-slim WORKDIR /app COPY --from=builder /install /install COPY app.py . ENV PYTHONPATH=/install/lib/python3.12/site-packages EXPOSE 5000 RUN groupadd -r appuser && useradd -r -g appuser appuser RUN chown -R appuser:appuser /app USER appuser CMD ["flask", "run", "--host=0.0.0.0"]
8.4 — Comparaison des tailles Commande : docker images | findstr app
Image | Taille app-naive:v1 | 1.62 GB app-optimisee:v1 | 184 MB
Gain : environ 1.44 GB soit ~89% de réduction. Raisons : python:3.12 (image complète) vs python:3.12-slim + le multi-stage build exclut tous les outils de build du stage final, seules les dépendances installées sont copiées.
8.5 — Utilisateur non-root Ajout dans le Dockerfile (stage final) : RUN groupadd -r appuser && useradd -r -g appuser appuser RUN chown -R appuser:appuser /app USER appuser
Vérification : docker run --rm app-optimisee:v1 whoami Résultat : appuser Le processus ne tourne plus en root.
8.6 — Risque de tourner en root dans un conteneur Si un attaquant exploite une faille dans l'application et sort de l'isolation du conteneur (escape), il obtiendrait les privilèges root sur la machine hôte, pouvant compromettre tout le système.
8.7 — Lister toutes les images triées par taille Commande : docker images --format "{{.Repository}}:{{.Tag}}\t{{.Size}}" | sort
Résultat obtenu : alpine:latest 13.1MB app-naive:v1 1.62GB app-optimisee:v1 184MB exercice-6-web 200MB flask-app:v1 197MB mon-site:v2 92.6MB nginx:alpine 93.5MB postgres:16-alpine 395MB redis:7-alpine 61.2MB