TP Docker – Des premiers conteneurs à Docker Compose
Niveau : débutant, aucun prérequis Docker Durée estimée : 4 h à 5 h Prérequis : savoir ouvrir un terminal, connaître les commandes de base (cd, ls, cat), avoir Docker Engine ou Docker Desktop installé.
Objectifs pédagogiques
À la fin de ce TP, vous devez être capable de :
- Expliquer la différence entre une image et un conteneur, et entre un conteneur et une machine virtuelle.
- Lancer, arrêter, inspecter et supprimer des conteneurs.
- Publier un port et rendre un service accessible depuis votre machine.
- Persister des données avec un bind mount et avec un volume nommé.
- Configurer un conteneur par variables d'environnement.
- Écrire un Dockerfile, construire une image et comprendre le cache de build.
- Faire communiquer plusieurs conteneurs sur un réseau Docker.
- Décrire une application multi-services dans un fichier
compose.yaml.
Comment travailler
Chaque module suit la même structure :
- Concept : ce qu'il faut comprendre avant de taper des commandes.
- Manipulation : les commandes à exécuter, dans l'ordre.
- Questions : à rédiger dans un fichier
reponses.mdque vous rendrez. - Point de contrôle : ce qui doit fonctionner avant de passer au module suivant.
Créez d'abord votre répertoire de travail :
mkdir -p ~/tp-docker && cd ~/tp-docker
touch reponses.md
Les réponses attendues sont en annexe B. Ne les consultez qu'après avoir cherché.
Module 0 – Vérifier l'environnement
Manipulation
docker version
docker info
docker run --rm hello-world
Si docker version affiche uniquement la partie Client avec une erreur de connexion, le démon n'est pas démarré ou votre utilisateur n'a pas les droits.
Sous Linux :
sudo systemctl status docker
sudo systemctl start docker
sudo usermod -aG docker $USER # puis se déconnecter / reconnecter
Questions
- Q0.1 Quelle version du client et quelle version du serveur (démon) avez-vous ?
- Q0.2 Dans la sortie de
docker info, relevez leStorage Driveret le nombre d'images présentes.
Point de contrôle
docker run --rm hello-world affiche le message de bienvenue.
Module 1 – Les concepts de base
Concept
Docker manipule quatre objets que vous allez retrouver en permanence.
L'image est un modèle en lecture seule. Elle contient un système de fichiers (les binaires, les bibliothèques, votre code) et des métadonnées (la commande à lancer, les variables d'environnement par défaut, les ports déclarés). Une image ne s'exécute pas, elle sert de patron.
Le conteneur est une instance en cours d'exécution d'une image. Docker ajoute au-dessus de l'image une couche en écriture, propre au conteneur. À partir d'une même image on peut lancer autant de conteneurs que l'on veut, ils sont indépendants les uns des autres.
Le registry est le dépôt d'images. Docker Hub est le registry public par défaut. Quand vous écrivez nginx:1.27-alpine, vous demandez le dépôt nginx avec le tag 1.27-alpine sur Docker Hub.
Les couches (layers) : une image est un empilement de couches, chacune correspondant à une étape de construction. Les couches sont partagées entre images, ce qui explique qu'une deuxième image basée sur debian:12 se télécharge beaucoup plus vite que la première.
Différence avec une machine virtuelle : une VM embarque un noyau complet et un système d'exploitation entier, gérés par un hyperviseur. Un conteneur partage le noyau de la machine hôte et n'isole que l'espace utilisateur, à l'aide de deux mécanismes du noyau Linux : les namespaces (isolation de la vue : processus, réseau, points de montage, nom de machine) et les cgroups (limitation des ressources : CPU, mémoire). Un conteneur démarre donc en quelques dizaines de millisecondes et pèse quelques dizaines de mégaoctets, contre plusieurs secondes et plusieurs gigaoctets pour une VM.
Conséquence importante : un conteneur Linux a besoin d'un noyau Linux. Sur Windows et macOS, Docker Desktop fait tourner une petite VM Linux en arrière-plan, et vos conteneurs s'exécutent dedans.
Manipulation
docker image ls
docker pull debian:12
docker pull nginx:1.27-alpine
docker image ls
docker image inspect nginx:1.27-alpine | head -n 40
Questions
- Q1.1 Quelle est la taille de l'image
debian:12? Et celle denginx:1.27-alpine? Comment expliquez-vous l'écart avec une VM Debian classique ? - Q1.2 Que se passe-t-il si vous relancez
docker pull debian:12? Pourquoi ? - Q1.3 Reformulez en une phrase la différence entre
nginx(le dépôt),1.27-alpine(le tag) et l'identifiantsha256:...(le digest).
Module 2 – Premier conteneur, cycle de vie
Concept
docker run enchaîne trois opérations : télécharger l'image si elle est absente, créer un conteneur à partir de cette image, puis le démarrer. Un conteneur vit tant que son processus principal (PID 1) tourne. Quand ce processus se termine, le conteneur s'arrête. Il n'est pas supprimé pour autant : il reste en état Exited et occupe de l'espace disque jusqu'à ce que vous le supprimiez.
Manipulation
# Premier plan : la sortie du conteneur s'affiche dans votre terminal
docker run debian:12 echo "bonjour depuis un conteneur"
# Le conteneur existe encore, à l'arrêt
docker ps
docker ps -a
# Arrière-plan (detached) avec un nom explicite
docker run -d --name veilleur debian:12 sleep 300
docker ps
docker logs veilleur
docker inspect veilleur | head -n 30
docker stats --no-stream veilleur
# Cycle de vie
docker stop veilleur
docker ps -a
docker start veilleur
docker restart veilleur
docker stop veilleur
docker rm veilleur
Testez maintenant la suppression automatique :
docker run --rm debian:12 echo "je disparais tout de suite"
docker ps -a
Questions
- Q2.1 Pourquoi
docker psne montre pas le conteneur créé pardocker run debian:12 echo ..., alors quedocker ps -ale montre ? - Q2.2 À quoi sert l'option
--rm? Dans quels cas est-elle une mauvaise idée ? - Q2.3 Lancez
docker run -d --name test debian:12. Le conteneur s'arrête immédiatement. Expliquez pourquoi (indice : quelle est la commande par défaut de l'imagedebian?). - Q2.4 Quelle est la différence entre
docker stopetdocker kill? Regardez la documentation sur les signauxSIGTERMetSIGKILL.
Point de contrôle
docker ps -a ne doit plus contenir aucun conteneur du module. Nettoyez avec docker rm <nom>.
Module 3 – Entrer dans un conteneur et observer l'isolation
Concept
Deux options changent tout pour travailler en interactif :
-i(--interactive) garde l'entrée standard ouverte,-t(--tty) alloue un pseudo-terminal.
On les combine presque toujours en -it.
Attention à ne pas confondre :
docker run -it <image> <commande>crée un nouveau conteneur,docker exec -it <conteneur> <commande>exécute une commande dans un conteneur déjà démarré.
Manipulation
docker run -it --name bac-a-sable debian:12 bash
Vous êtes maintenant dans le conteneur. Exécutez :
hostname
ps aux
ls /
cat /etc/os-release
ip addr || (apt-get update && apt-get install -y iproute2 && ip addr)
touch /fichier-temoin
echo "modification" > /root/note.txt
exit
De retour sur l'hôte :
docker start bac-a-sable
docker exec -it bac-a-sable bash
ls /fichier-temoin && cat /root/note.txt
exit
docker rm -f bac-a-sable
docker run -it --rm debian:12 bash -c "ls /fichier-temoin"
Questions
- Q3.1 Combien de processus voyez-vous avec
ps auxdans le conteneur ? Comparez avecps aux | wc -lsur votre machine. Quel namespace explique cette différence ? - Q3.2 Le fichier
/fichier-temoinexiste-t-il aprèsdocker start+docker exec? Existe-t-il dans un nouveau conteneur créé depuis la même image ? Qu'en déduisez-vous sur la couche d'écriture du conteneur ? - Q3.3 Si vous supprimez le conteneur, que deviennent les données écrites dedans ?
- Q3.4 Le
hostnamedu conteneur correspond à quoi ?
Module 4 – Publier un service sur un port
Concept
Chaque conteneur reçoit sa propre pile réseau et sa propre adresse IP sur un réseau interne. Cette adresse n'est pas routable depuis l'extérieur de la machine hôte. Pour rendre un service accessible, on publie un port avec -p <port_hote>:<port_conteneur>.
L'instruction EXPOSE présente dans une image est purement documentaire : elle indique quel port le service écoute, elle n'ouvre rien.
Manipulation
docker run -d --name web -p 8080:80 nginx:1.27-alpine
docker ps
curl -I http://localhost:8080
docker logs web
docker logs -f web # Ctrl+C pour quitter le suivi
Ouvrez http://localhost:8080 dans un navigateur, rechargez plusieurs fois, puis regardez les logs.
Testez un second conteneur sur le même port hôte :
docker run -d --name web2 -p 8080:80 nginx:1.27-alpine
Puis corrigez :
docker rm web2
docker run -d --name web2 -p 8081:80 nginx:1.27-alpine
curl -I http://localhost:8081
Questions
- Q4.1 Quelle erreur exacte obtenez-vous en publiant deux fois le port hôte 8080 ? Pourquoi les deux conteneurs peuvent-ils en revanche écouter tous les deux sur le port 80 en interne ?
- Q4.2 Que donne
docker port web? - Q4.3 Que se passe-t-il avec
docker run -d --name web3 nginx:1.27-alpine(sans-p) puiscurl http://localhost:80? Le serveur tourne-t-il pour autant ? - Q4.4 Quelle différence entre
-p 8080:80et-p 127.0.0.1:8080:80? Laquelle choisiriez-vous sur un serveur exposé à Internet ?
Point de contrôle
docker rm -f web web2 web3 2>/dev/null
Module 5 – Persister les données
Concept
La couche d'écriture d'un conteneur disparaît avec lui. Docker propose deux mécanismes pour sortir les données du cycle de vie du conteneur.
Le bind mount monte un répertoire de votre machine dans le conteneur : -v /chemin/hote:/chemin/conteneur. Le contenu de l'hôte masque celui du conteneur. C'est l'outil du développement, pour éditer du code sans reconstruire l'image.
Le volume nommé est un espace géré par Docker (sous /var/lib/docker/volumes/ sous Linux) : -v mon-volume:/chemin/conteneur. Vous ne gérez pas le chemin hôte, Docker s'en charge. C'est l'outil de la production, pour les données de bases de données notamment.
Manipulation, partie A : bind mount
mkdir -p ~/tp-docker/site
cat > ~/tp-docker/site/index.html <<'EOF'
<!doctype html>
<html lang="fr">
<head><meta charset="utf-8"><title>TP Docker</title></head>
<body><h1>Version 1</h1></body>
</html>
EOF
docker run -d --name site \
-p 8080:80 \
-v ~/tp-docker/site:/usr/share/nginx/html:ro \
nginx:1.27-alpine
curl http://localhost:8080
Modifiez le fichier sur l'hôte, sans toucher au conteneur :
sed -i 's/Version 1/Version 2/' ~/tp-docker/site/index.html
curl http://localhost:8080
Testez l'option :ro (lecture seule) :
docker exec -it site sh -c "echo test > /usr/share/nginx/html/test.html"
Manipulation, partie B : volume nommé
docker volume create donnees
docker volume ls
docker volume inspect donnees
docker run --rm -v donnees:/data debian:12 sh -c "echo 'écrit par le conteneur A' > /data/message.txt"
docker run --rm -v donnees:/data debian:12 cat /data/message.txt
Le premier conteneur a été supprimé, la donnée est toujours là.
Questions
- Q5.1 Quelle erreur obtenez-vous en écrivant dans un montage
:ro? - Q5.2 Où se trouve physiquement le volume
donneessur l'hôte (utilisezdocker volume inspect) ? - Q5.3 Citez deux cas où un bind mount est préférable, et deux cas où un volume nommé est préférable.
- Q5.4 Que se passe-t-il si vous montez un volume vide sur un répertoire du conteneur qui contenait déjà des fichiers ? Et avec un bind mount vide ? Testez avec
-v vide:/etc/nginxsur l'image nginx.
Point de contrôle
docker rm -f site
Module 6 – Configurer par variables d'environnement
Concept
Une image doit être générique et une configuration doit être injectée au démarrage, pas cuite dans l'image. Le vecteur standard est la variable d'environnement, avec -e CLE=valeur ou --env-file fichier.
C'est ainsi que se configurent la plupart des images officielles : mots de passe, noms de bases, options de démarrage.
Manipulation
docker run -d --name postgres \
-e POSTGRES_USER=tp \
-e POSTGRES_PASSWORD=motdepasse \
-e POSTGRES_DB=tpdb \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16-alpine
docker logs postgres | tail -n 20
docker exec -it postgres psql -U tp -d tpdb
Dans psql :
CREATE TABLE visites (id serial PRIMARY KEY, vu_le timestamptz DEFAULT now());
INSERT INTO visites DEFAULT VALUES;
INSERT INTO visites DEFAULT VALUES;
SELECT * FROM visites;
\q
Détruisez le conteneur, recréez-le avec le même volume :
docker rm -f postgres
docker run -d --name postgres \
-e POSTGRES_USER=tp \
-e POSTGRES_PASSWORD=motdepasse \
-e POSTGRES_DB=tpdb \
-v pgdata:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16-alpine
docker exec -it postgres psql -U tp -d tpdb -c "SELECT count(*) FROM visites;"
Regardez aussi les variables vues par le conteneur :
docker exec postgres env
Questions
- Q6.1 Vos deux lignes sont-elles toujours présentes après suppression et recréation du conteneur ? Pourquoi ?
- Q6.2 Que se passe-t-il si vous omettez
POSTGRES_PASSWORD? Lisez le message d'erreur dansdocker logs. - Q6.3 Pourquoi est-il déconseillé d'écrire un mot de passe directement dans un
docker runou dans un Dockerfile ? Quelles alternatives connaissez-vous ? - Q6.4 Que trouve-t-on dans la sortie de
docker exec postgres enven plus de vos variables ? D'où viennent les autres ?
Point de contrôle
Gardez le conteneur postgres en marche, il resservira au module 9.
Module 7 – Construire sa propre image
Concept
Un Dockerfile est une recette. Chaque instruction produit une couche. Les principales :
| Instruction | Rôle |
|---|---|
FROM |
image de base |
WORKDIR |
répertoire de travail des instructions suivantes |
COPY |
copie des fichiers du contexte de build vers l'image |
RUN |
exécute une commande au moment du build |
ENV |
variable d'environnement par défaut |
EXPOSE |
documente le port écouté |
CMD |
commande par défaut au démarrage du conteneur |
ENTRYPOINT |
exécutable fixe, CMD en devient les arguments |
Le contexte de build est le répertoire passé en dernier argument de docker build (souvent .). Il est envoyé au démon avant la construction, d'où l'intérêt du fichier .dockerignore.
Le cache : Docker réutilise une couche si l'instruction et son entrée n'ont pas changé. Dès qu'une couche est invalidée, toutes les suivantes le sont. D'où la règle : ce qui change rarement se place en haut du Dockerfile, ce qui change souvent en bas.
Manipulation
mkdir -p ~/tp-docker/app && cd ~/tp-docker/app
app.py :
import os
from flask import Flask
app = Flask(__name__)
MESSAGE = os.environ.get("MESSAGE", "Bonjour depuis mon image")
@app.route("/")
def index():
return f"{MESSAGE}\n"
@app.route("/sante")
def sante():
return {"statut": "ok"}
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
requirements.txt :
flask==3.0.3
Dockerfile :
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
ENV MESSAGE="Bonjour depuis mon image"
EXPOSE 8000
CMD ["python", "app.py"]
.dockerignore :
.git
__pycache__
*.pyc
.venv
reponses.md
Construisez et lancez :
docker build -t monapp:1.0 .
docker image ls monapp
docker history monapp:1.0
docker run -d --name monapp -p 8000:8000 monapp:1.0
curl http://localhost:8000
curl http://localhost:8000/sante
docker run -d --name monapp-fr -p 8001:8000 -e MESSAGE="Message surchargé" monapp:1.0
curl http://localhost:8001
Observez le cache. Modifiez uniquement app.py :
sed -i 's/Bonjour depuis mon image/Bonjour version 2/' app.py
docker build -t monapp:2.0 .
Puis inversez volontairement l'ordre des instructions (mettez COPY . . avant le RUN pip install) et reconstruisez pour comparer.
Questions
- Q7.1 Combien d'étapes sont marquées
CACHEDlors du second build ? Lesquelles et pourquoi ? - Q7.2 Pourquoi copie-t-on
requirements.txtseul avant de copier le code applicatif ? - Q7.3 Quelle est la différence entre
RUNetCMD? Et entreCMDetENTRYPOINT? - Q7.4 À quoi sert
--no-cache-dirdans lepip install? Quel est l'effet sur la taille de l'image ? - Q7.5 Comparez la taille de
monapp:1.0avec celle d'une image basée surpython:3.12(sans-slim). Refaites le build pour mesurer. - Q7.6 Que contient
docker history monapp:1.0? Quelle instruction pèse le plus lourd ?
Point de contrôle
curl http://localhost:8000 renvoie votre message, curl http://localhost:8001 renvoie le message surchargé.
Module 8 – Les réseaux Docker
Concept
Docker crée par défaut trois réseaux : bridge (celui utilisé quand vous ne précisez rien), host et none.
Point clé pour la suite : sur le réseau bridge par défaut, les conteneurs ne se résolvent pas par leur nom. Sur un réseau défini par l'utilisateur, Docker fournit un DNS interne, et le nom du conteneur devient un nom d'hôte utilisable. C'est la raison pour laquelle on crée toujours un réseau dédié à une application.
Manipulation
docker network ls
docker network create tp-net
docker network inspect tp-net
# Un serveur et un client sur le réseau dédié
docker run -d --name api --network tp-net monapp:1.0
docker run --rm -it --network tp-net curlimages/curl:8.10.1 curl http://api:8000/
# Comparaison sur le bridge par défaut
docker run -d --name api-defaut monapp:1.0
docker run --rm -it curlimages/curl:8.10.1 curl --max-time 5 http://api-defaut:8000/
Connectez le conteneur postgres du module 6 au réseau :
docker network connect tp-net postgres
docker run --rm -it --network tp-net postgres:16-alpine \
psql "postgresql://tp:motdepasse@postgres:5432/tpdb" -c "SELECT count(*) FROM visites;"
Questions
- Q8.1 Le
curl http://api:8000/fonctionne-t-il surtp-net? Et sur le bridge par défaut avecapi-defaut? Expliquez. - Q8.2 Le conteneur
apia été lancé sans-p. Pourquoi est-il quand même joignable depuis l'autre conteneur ? - Q8.3 Sur
tp-net, quel port utilise-t-on pour joindre l'API : le port hôte ou le port du conteneur ? - Q8.4 Quel est l'intérêt, en production, de ne pas publier le port d'une base de données sur l'hôte ?
Point de contrôle
docker rm -f api api-defaut monapp monapp-fr postgres
Module 9 – Docker Compose
Concept
Reproduire à la main une pile de trois conteneurs avec leurs réseaux, volumes et variables devient vite ingérable. Compose décrit l'ensemble dans un fichier YAML versionnable, et pilote le tout avec docker compose up / docker compose down.
Compose crée automatiquement un réseau dédié au projet. Les services s'y résolvent par leur nom de service.
Manipulation
mkdir -p ~/tp-docker/compose && cd ~/tp-docker/compose
mkdir -p app
app/requirements.txt :
flask==3.0.3
psycopg[binary]==3.2.3
app/app.py :
import os
import time
import psycopg
from flask import Flask
app = Flask(__name__)
DSN = os.environ["DATABASE_URL"]
def connexion():
derniere = None
for _ in range(15):
try:
return psycopg.connect(DSN)
except psycopg.OperationalError as exc:
derniere = exc
time.sleep(2)
raise derniere
def init():
with connexion() as conn:
conn.execute(
"CREATE TABLE IF NOT EXISTS compteur ("
"id int PRIMARY KEY, valeur int NOT NULL)"
)
conn.execute(
"INSERT INTO compteur (id, valeur) VALUES (1, 0) "
"ON CONFLICT (id) DO NOTHING"
)
conn.commit()
@app.route("/")
def index():
with connexion() as conn:
cur = conn.execute(
"UPDATE compteur SET valeur = valeur + 1 WHERE id = 1 RETURNING valeur"
)
valeur = cur.fetchone()[0]
conn.commit()
return f"Nombre de visites : {valeur}\n"
init()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
app/Dockerfile :
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
compose.yaml :
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: tp
POSTGRES_PASSWORD: motdepasse
POSTGRES_DB: tpdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tp -d tpdb"]
interval: 5s
timeout: 3s
retries: 10
web:
build: ./app
environment:
DATABASE_URL: postgresql://tp:motdepasse@db:5432/tpdb
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
volumes:
pgdata:
Lancez la pile :
docker compose up -d --build
docker compose ps
docker compose logs -f web # Ctrl+C pour quitter
curl http://localhost:8000
curl http://localhost:8000
curl http://localhost:8000
docker compose exec db psql -U tp -d tpdb -c "SELECT * FROM compteur;"
Testez la persistance :
docker compose down
docker compose up -d
curl http://localhost:8000 # le compteur repart-il de zéro ?
docker compose down -v
docker compose up -d
curl http://localhost:8000
Questions
- Q9.1 Quel nom porte le réseau créé par Compose ? Utilisez
docker network ls. D'où vient ce nom ? - Q9.2 Dans
DATABASE_URL, l'hôte estdb. À quoi correspond ce nom ? - Q9.3 Quelle différence entre
docker compose downetdocker compose down -v? Constatez l'effet sur le compteur. - Q9.4 À quoi sert le
healthcheckcombiné àdepends_on: condition: service_healthy? Que se passerait-il sans, au premier démarrage ? - Q9.5 Le service
dbn'a pas de sectionports. Est-il joignable depuis votre machine ? Depuisweb? Pourquoi ? - Q9.6 Quelle est la différence entre
image:etbuild:dans un service Compose ?
Point de contrôle
curl http://localhost:8000 incrémente le compteur à chaque appel, et le compteur survit à un docker compose down suivi d'un up.
Module 10 – Nettoyage et bonnes pratiques
Concept
Les images, conteneurs arrêtés, volumes orphelins et caches de build s'accumulent. Sur un poste de développement, plusieurs dizaines de gigaoctets sont vite atteints.
Manipulation
docker system df
docker system df -v
docker container prune
docker image prune # images "dangling" seulement
docker image prune -a # toutes les images non utilisées, prudence
docker builder prune
docker volume ls -f dangling=true
docker volume prune
docker system prune -a --volumes # à ne jamais lancer sans savoir ce que l'on supprime
Bonnes pratiques à retenir
- Toujours épingler une version d'image (
postgres:16-alpine, paspostgres:latest). - Ne jamais stocker de secret dans une image ni dans un Dockerfile.
- Un conteneur, un rôle. Ne pas empiler application et base dans le même conteneur.
- Écrire les logs sur la sortie standard, pas dans un fichier interne au conteneur.
- Traiter les conteneurs comme jetables. Toute donnée à conserver va dans un volume.
- Ordonner le Dockerfile du plus stable au plus volatil pour exploiter le cache.
- Utiliser un
.dockerignorepour réduire le contexte de build. - Ne pas exécuter le processus en
rootdans l'image finale.
Questions
- Q10.1 Combien d'espace récupérez-vous avec
docker system dfavant et après nettoyage ? - Q10.2 Que signifie une image « dangling » ?
- Q10.3 Pourquoi
docker volume pruneest-il la commande la plus risquée de la liste ?
Module 11 – Pour aller plus loin (optionnel)
A. Build multi-étapes
Objectif : ne livrer que le nécessaire dans l'image finale. Adaptez ce modèle à l'application du module 9.
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
ENV PYTHONPATH=/deps
COPY --from=build /deps /deps
COPY app.py .
RUN useradd --create-home --uid 10001 appuser
USER appuser
EXPOSE 8000
CMD ["python", "app.py"]
- Q11.1 Quelle taille fait l'image multi-étapes par rapport à l'image du module 9 ?
- Q11.2 Vérifiez avec
docker exec <conteneur> idque le processus ne tourne pas enroot.
B. Limiter les ressources
docker run -d --name limite --memory=256m --cpus=0.5 monapp:1.0
docker stats --no-stream limite
- Q11.3 Quel mécanisme du noyau applique ces limites ?
C. Publier une image
docker login
docker tag monapp:1.0 <votre-compte>/monapp:1.0
docker push <votre-compte>/monapp:1.0
Module 12 – Mini-projet de synthèse
À rendre dans un dossier projet/ contenant tout le nécessaire pour qu'un tiers lance l'application avec une seule commande.
Cahier des charges
Réalisez une petite application de prise de notes composée de trois services :
- api : une application Python (Flask ou FastAPI) que vous construisez avec votre propre Dockerfile. Elle expose :
POST /notesavec un corps JSON{"texte": "..."}qui enregistre une note,GET /notesqui renvoie la liste des notes,GET /santequi renvoie{"statut": "ok"}.
- db : PostgreSQL, avec un volume nommé pour les données, aucun port publié sur l'hôte.
- proxy : nginx qui écoute sur le port 8080 de l'hôte et transmet les requêtes à
api.
Contraintes
- Toute la pile démarre avec
docker compose up -d. - La configuration de la base passe par des variables d'environnement, lues depuis un fichier
.envnon versionné. Un.env.exampleest fourni. - Les données survivent à
docker compose downpuisup. - L'image de l'API est construite en multi-étapes et le processus ne tourne pas en
root. - Un
healthcheckest défini surdbet surapi. - Un fichier
README.mdexplique le lancement, l'arrêt, la consultation des logs et la remise à zéro.
Grille d'évaluation indicative
| Critère | Points |
|---|---|
| La pile démarre en une commande | 4 |
| Persistance des données vérifiée | 3 |
| Dockerfile correct et cache exploité | 3 |
Aucun secret en dur, .env utilisé |
3 |
Réseau : db non exposé, résolution par nom de service |
3 |
| Healthchecks fonctionnels | 2 |
| Utilisateur non root, image allégée | 2 |
| README clair | 2 |
| Réponses aux questions des modules 0 à 10 | 8 |
Annexe A – Aide-mémoire
# Images
docker image ls # lister
docker pull <image>:<tag> # télécharger
docker build -t <nom>:<tag> . # construire
docker image rm <image> # supprimer
docker history <image> # couches
# Conteneurs
docker run -d --name <nom> -p 8080:80 <image>
docker ps / docker ps -a
docker logs -f <conteneur>
docker exec -it <conteneur> sh
docker inspect <conteneur>
docker stop / start / restart / rm <conteneur>
docker rm -f <conteneur> # forcer
docker stats
# Volumes
docker volume create <nom>
docker volume ls / inspect / rm
# Réseaux
docker network create <nom>
docker network ls / inspect
docker network connect <reseau> <conteneur>
# Compose
docker compose up -d --build
docker compose ps
docker compose logs -f <service>
docker compose exec <service> sh
docker compose down [-v]
# Nettoyage
docker system df
docker system prune -a --volumes
Annexe B – Réponses attendues
Q0.2 Le storage driver est généralement overlay2 sous Linux.
Q1.1 debian:12 fait environ 120 Mo, nginx:1.27-alpine environ 50 Mo. Une image ne contient pas de noyau ni les services système d'un OS complet, seulement l'espace utilisateur nécessaire.
Q1.2 Rien n'est retéléchargé, les couches sont déjà présentes localement.
Q1.3 Le dépôt identifie le projet, le tag est une étiquette mobile pointant vers une version, le digest est l'empreinte immuable du contenu exact de l'image.
Q2.1 Le conteneur s'est arrêté dès la fin de echo. docker ps ne montre que les conteneurs en cours d'exécution.
Q2.2 --rm supprime le conteneur à son arrêt. À éviter quand on veut consulter les logs ou l'état après l'arrêt, notamment pour diagnostiquer un plantage.
Q2.3 La commande par défaut de l'image debian est bash. Sans -it, bash ne trouve pas d'entrée standard et se termine immédiatement, donc le conteneur s'arrête.
Q2.4 docker stop envoie SIGTERM puis, après un délai de grâce (10 s par défaut), SIGKILL. docker kill envoie directement SIGKILL, sans laisser le processus se terminer proprement.
Q3.1 Très peu de processus, souvent un seul plus ps. C'est le namespace PID qui isole la table des processus.
Q3.2 Le fichier survit à stop/start du même conteneur car il est dans la couche d'écriture de ce conteneur. Il est absent d'un nouveau conteneur, qui repart de l'image d'origine.
Q3.3 Elles sont perdues avec la couche d'écriture.
Q3.4 Au début de l'identifiant du conteneur, sauf si --hostname est précisé.
Q4.1 Une erreur de type « port is already allocated ». Les ports 80 internes n'entrent pas en conflit car chaque conteneur a son propre namespace réseau, donc sa propre pile de ports.
Q4.3 Aucune réponse sur l'hôte, mais nginx tourne bien dans le conteneur. Il est joignable depuis un autre conteneur du même réseau, ou via l'IP du conteneur.
Q4.4 -p 8080:80 écoute sur toutes les interfaces de l'hôte, -p 127.0.0.1:8080:80 seulement en local. Sur un serveur exposé, la seconde forme, avec un reverse proxy devant.
Q5.1 Un refus d'écriture, le système de fichiers étant monté en lecture seule.
Q5.3 Bind mount : développement avec rechargement à chaud, injection d'un fichier de configuration depuis le dépôt. Volume nommé : données d'une base, données applicatives d'un service en production.
Q5.4 Avec un volume nommé vide, Docker recopie le contenu initial du répertoire de l'image dans le volume au premier montage. Avec un bind mount, aucune recopie : le répertoire de l'hôte masque le contenu de l'image, et nginx ne trouve plus sa configuration.
Q6.1 Oui, les données sont dans le volume pgdata, indépendant du conteneur.
Q6.2 Postgres refuse de démarrer et exige POSTGRES_PASSWORD ou POSTGRES_HOST_AUTH_METHOD=trust.
Q6.3 Le mot de passe apparaît dans l'historique du shell, dans docker inspect et dans les couches de l'image. Alternatives : fichier .env non versionné, secrets Docker Swarm ou Kubernetes, gestionnaire de secrets externe.
Q6.4 On y trouve PATH, HOSTNAME, et les variables définies par les instructions ENV de l'image de base.
Q7.1 Les étapes jusqu'au pip install sont en cache, car ni requirements.txt ni les instructions précédentes n'ont changé. Seules la copie de app.py et les étapes suivantes sont rejouées.
Q7.2 Pour que la couche d'installation des dépendances, longue, ne soit invalidée que lorsque les dépendances changent, et non à chaque modification du code.
Q7.3 RUN s'exécute pendant le build et produit une couche. CMD définit la commande de démarrage du conteneur, remplaçable en ligne de commande. ENTRYPOINT définit l'exécutable fixe, CMD fournissant alors ses arguments par défaut.
Q7.4 Il évite de conserver le cache de téléchargement de pip dans la couche, ce qui réduit la taille de l'image.
Q7.5 L'image basée sur python:3.12 complet est environ trois à quatre fois plus lourde.
Q8.1 La résolution par nom fonctionne sur tp-net grâce au DNS embarqué des réseaux définis par l'utilisateur. Sur le bridge par défaut, le nom n'est pas résolu et curl échoue.
Q8.2 La publication de port ne concerne que l'accès depuis l'hôte. Entre conteneurs d'un même réseau, tous les ports sont joignables directement.
Q8.3 Le port du conteneur, ici 8000.
Q8.4 Cela réduit la surface d'attaque : la base n'est accessible que depuis les services de son réseau.
Q9.1 Il s'appelle <nom_du_dossier>_default, le nom de projet étant par défaut celui du répertoire.
Q9.2 Au nom du service Compose, résolu par le DNS interne du réseau du projet.
Q9.3 down supprime conteneurs et réseau mais conserve les volumes nommés. down -v supprime aussi les volumes, donc le compteur repart de zéro.
Q9.4 Il retarde le démarrage de web jusqu'à ce que Postgres accepte réellement les connexions. Sans lui, web peut démarrer avant que la base soit prête, d'où l'intérêt aussi de la boucle de reconnexion dans le code.
Q9.5 Non depuis l'hôte, oui depuis web via le réseau du projet.
Q9.6 image: utilise une image existante, build: construit l'image à partir d'un Dockerfile local.
Q10.2 Une image sans tag, remplacée par un nouveau build portant le même nom.
Q10.3 Elle supprime les volumes non référencés par un conteneur, donc potentiellement des données de production dont le conteneur a été supprimé temporairement.
Q11.3 Les cgroups.