Outils du dev · Tuto

Docker, du premier conteneur à Compose
Lancer un service sans rien installer sur votre machine, écrire un Dockerfile propre, relire celui qu'une IA vous propose, puis faire tourner plusieurs services ensemble. Les commandes sont vérifiées dans la documentation officielle de Docker.
Aller plus loin avec01Ce que Docker règle
Un projet tourne sur votre poste et plante chez le collègue ou sur le serveur. Une version de PHP, de Node ou de Postgres diffère, une bibliothèque manque. Docker emballe l'application avec tout ce dont elle a besoin, et ce paquet se lance de la même façon partout.
Ce paquet qui tourne s'appelle un conteneur. C'est un processus isolé du reste de la machine, avec ses propres fichiers, mais qui partage le noyau du système hôte. Une machine virtuelle, elle, embarque un système complet. Le conteneur démarre donc en quelques secondes et pèse bien moins lourd.
Les conteneurs Docker reposent sur le noyau Linux. Sur Mac et sur Windows, Docker Desktop fait tourner une petite machine virtuelle Linux en arrière-plan, sans que vous ayez à vous en occuper.
02L'image et le conteneur
L'image est un modèle figé : des fichiers et des réglages, en lecture seule. Le conteneur est une image en train de tourner. Une même image peut donner dix conteneurs, comme un moule donne dix pièces identiques. Supprimer un conteneur ne touche pas à l'image.
Pour essayer, lancez un serveur web nginx. Docker télécharge l'image la première fois, puis démarre le conteneur.
docker run -d -p 8080:80 --name mon-nginx nginx:1.29-alpine
Ouvrez http://localhost:8080 : la page d'accueil de nginx s'affiche. L'option -d lance le conteneur en arrière-plan, -p relie un port de votre machine à un port du conteneur, --name lui donne un nom.
docker psListe les conteneurs en cours d'exécution, avec leur nom, leur image et leurs ports.
docker imagesListe les images téléchargées ou construites sur votre machine, avec leur taille.
docker stop mon-nginxArrête le conteneur. Il existe toujours et peut redémarrer avec docker start.
docker rm mon-nginxSupprime le conteneur arrêté. L'image reste là, prête pour le suivant.
03Les tags et les images légères
Les images publiques viennent en général de Docker Hub. Dans nginx:1.29-alpine, ce qui suit les deux-points est le tag : il désigne une version et une variante de l'image.
Sans tag, Docker prend latest. Ce nom ne garantit rien : c'est juste le tag par défaut, que l'éditeur de l'image déplace quand il veut. Le jour où il passe à une version majeure, votre build change sans prévenir. Écrivez toujours une version.
La plupart des images existent aussi en variante légère. -slim retire ce qui sert rarement, -alpine part d'Alpine Linux, une distribution minuscule. Voici les tailles compressées affichées par Docker Hub le 31 octobre 2026, pour les processeurs Intel et AMD (amd64).
| Image | Taille compressée |
|---|---|
python:3.12 | 412 Mo |
python:3.12-slim | 43 Mo |
python:3.12-alpine | 18 Mo |
node:24 | 410 Mo |
node:24-slim | 81 Mo |
node:24-alpine | 62 Mo |
Alpine utilise la bibliothèque C musl à la place de glibc. Certains paquets compilés pour glibc, fréquents en Python, s'y installent mal ou pas du tout. Partez de -slim par défaut et gardez -alpine pour les images que vous savez compatibles, comme nginx.
04Les données et les réglages
Un conteneur est jetable. Tout ce qu'il écrit dans ses propres fichiers disparaît avec lui. Pour une base de données, c'est la catastrophe assurée : les données doivent vivre ailleurs, dans un volume.
Un volume nommé est géré par Docker et survit à la suppression du conteneur. On le crée, puis on le monte à l'endroit où l'application range ses données. Pour Postgres 17, c'est /var/lib/postgresql/data.
docker volume create pg-data
docker run -d --name db -e POSTGRES_PASSWORD=[mot-de-passe] -v pg-data:/var/lib/postgresql/data postgres:17-alpineLe bind mount monte un dossier de votre machine dans le conteneur. Une modification de fichier se voit tout de suite, sans reconstruire. C'est l'outil du développement local : la commande suivante sert le dossier courant comme un site statique.
docker run --rm -p 8080:80 -v "$PWD":/usr/share/nginx/html:ro nginx:1.29-alpine
Les réglages passent par des variables d'environnement, une par une avec -e DEBUG=false, ou toutes ensemble depuis un fichier avec --env-file .env. La même image sert alors en local et en production, seules les variables changent.
Aucun secret dans l'image. Une valeur posée par ENV dans le Dockerfile reste inscrite dans l'image, lisible par quiconque la récupère. Passez les mots de passe au lancement, et ajoutez le fichier .env aux deux listes d'exclusion, .gitignore et .dockerignore.
05Les commandes de tous les jours
L'option -p se lit de gauche à droite : port de votre machine, puis port du conteneur. -p 8080:80 envoie ce qui arrive sur le port 8080 de l'hôte vers le port 80 du conteneur.
--rmSupprime le conteneur dès qu'il s'arrête.
QuandPour un essai ou une commande ponctuelle.
-itOuvre un terminal interactif dans le conteneur.
Exempledocker run --rm -it python:3.12-slim bash
Dans le conteneur, l'application doit écouter sur 0.0.0.0. Si elle écoute sur localhost, elle ne répond qu'à l'intérieur du conteneur, et le port publié ne mène nulle part.
Sur un serveur, attention aux ports publiés. Par défaut, Docker publie un port sur toutes les adresses de la machine, et ses règles passent avant celles du pare-feu ufw. Un -p 5432:5432 ouvre donc la base à tout internet, même si ufw la bloque. Ne publiez pas le port d'une base, ou limitez-le à la machine avec -p 127.0.0.1:5432:5432. La documentation détaille le cas dans sa page sur Docker et les pare-feu.
Pour savoir ce qui tourne et regarder à l'intérieur :
docker ps -aListe aussi les conteneurs arrêtés, avec leur code de sortie. Un conteneur qui a planté y figure encore.
docker logs -f --tail 100 webAffiche les 100 dernières lignes du journal, puis suit les nouvelles en direct.
QuandPremier réflexe quand un conteneur refuse de démarrer.
docker exec -it web shOuvre un shell dans un conteneur qui tourne. docker exec web env affiche ses variables d'environnement.
docker inspect webDonne toute la configuration du conteneur en JSON : réseau, volumes, variables, commande lancée.
docker statsMontre en continu le processeur, la mémoire et le réseau consommés par chaque conteneur.
Docker lit ce que l'application écrit sur sa sortie standard. Une application qui journalise dans un fichier interne laisse docker logs vide : configurez-la pour écrire sur la console.
Pour arrêter et faire le ménage :
docker stop webEnvoie le signal SIGTERM au processus principal, puis le tue avec SIGKILL s'il n'est pas sorti au bout de 10 secondes sous Linux, 30 sous Windows.
docker system dfMontre la place prise par les images, les conteneurs, les volumes et le cache de construction.
docker image pruneSupprime les images sans tag, restes des constructions successives.
docker system pruneSupprime les conteneurs arrêtés, les réseaux inutilisés, les images sans tag et le cache de construction inutilisé. Avec -a, toutes les images qu'aucun conteneur n'utilise.
Les volumes sont épargnés par défaut. L'option --volumes de docker system prune ajoute les volumes anonymes inutilisés. Un volume nommé comme pg-data ne part qu'avec une commande explicite, docker volume rm pg-data.
06Écrire un Dockerfile
Le Dockerfile est la recette de votre image. Voici celui d'une petite application Flask servie par gunicorn. Le fichier requirements.txt liste flask et gunicorn, et app.py définit un objet app.
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd --create-home appuser USER appuser EXPOSE 8000 CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]
Ligne par ligne : FROM choisit l'image de départ, WORKDIR fixe le dossier de travail. On copie d'abord la liste des dépendances, on les installe, puis on copie le code. useradd et USER font tourner l'application sans les droits root. EXPOSE documente le port écouté sans le publier. CMD donne la commande lancée au démarrage du conteneur.
docker build -t mon-appli:1.0 .
docker run -d -p 8000:8000 -e DATABASE_URL=[url-de-la-base] mon-appli:1.0Retenez la différence de moment. FROM, WORKDIR, COPY, RUN, ENV, ARG et USER agissent pendant la construction de l'image. CMD, ENTRYPOINT et HEALTHCHECK décrivent ce qui se passera quand un conteneur tournera.
07Les couches et le cache
Chaque instruction RUN, COPY ou ADD ajoute une couche à l'image. Au build suivant, Docker réutilise une couche tant que rien n'a changé pour elle. Dès qu'une couche change, toutes celles du dessus sont reconstruites.
D'où l'ordre du Dockerfile précédent : les dépendances, qui changent rarement, avant le code, qui change à chaque modification. Copiez tout le code en premier et le moindre changement d'un fichier relance l'installation complète des dépendances.
docker build --no-cache -t mon-appli .Reconstruit tout sans utiliser le cache.
QuandPour récupérer les dernières mises à jour de sécurité des paquets.
docker history mon-appli:1.0Liste les couches de l'image, l'instruction qui a créé chacune et sa taille.
Une couche ne s'efface jamais. Un fichier copié dans une couche puis supprimé dans une suivante reste dans l'image et alourdit son poids. C'est pour cela que la mise à jour des paquets, leur installation et le nettoyage tiennent dans une seule instruction RUN, sur une même couche.
RUN apt-get update && apt-get install -y --no-install-recommends [paquet] \
&& rm -rf /var/lib/apt/lists/*Le fichier .dockerignore, à la racine du projet, liste ce que COPY . . ne doit pas envoyer. Il protège vos secrets, évite qu'un dossier inutile casse le cache, et empêche de copier des dépendances compilées pour votre système plutôt que pour Linux.
.git .env .env.* node_modules dist __pycache__/ *.pyc .venv/
Quand l'application se compile, le multi-stage sépare la construction de la livraison. Un premier étage installe les outils et compile. Le second ne récupère que le résultat. Ici, un site statique construit avec Node, puis servi par nginx : l'image finale ne contient ni Node ni node_modules.
FROM node:24-slim AS build WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . RUN npm run build FROM nginx:1.29-alpine COPY --from=build /app/dist /usr/share/nginx/html
Une compilation lourde peut réclamer plusieurs gigaoctets de mémoire. Sur un petit serveur, construisez l'image ailleurs, sur votre poste ou dans un service d'intégration continue, et ne faites que la récupérer en production.
08Relire un Dockerfile écrit par une IA
Un assistant IA écrit un Dockerfile qui fonctionne du premier coup, et qui cache souvent deux ou trois défauts. Passez-le à cette grille avant de l'adopter.
- Un tag précis sur chaque
FROM, jamaislatestni rien du tout. - Une variante légère,
-slimen général. - Les dépendances avant le code, pour profiter du cache.
- Un .dockerignore à côté du Dockerfile.
- Aucun secret dans
ENV,ARGou un fichier copié. - apt-get groupé dans un seul
RUN, avec le nettoyage. - Un USER non root. Les images officielles
pythonetnodetournent en root par défaut. - CMD en forme exec, entre crochets, pour que l'application reçoive les signaux d'arrêt.
- Un seul rôle par conteneur : l'application d'un côté, la base de l'autre.
- EXPOSE n'ouvre rien. Seul
-p, ouports:dans Compose, publie un port. - Un multi-stage dès qu'il y a une étape de compilation.
La forme exec mérite une explication. En forme shell, CMD gunicorn app:app est lancé par /bin/sh -c. Le shell devient le processus principal et ne transmet pas SIGTERM à votre application. docker stop attend alors ses 10 secondes, puis coupe net, sans laisser l'application finir ses requêtes.
La forme exec ne suffit pas toujours. Un programme lancé comme processus principal ignore SIGTERM s'il ne prévoit rien pour le traiter, et certains serveurs de développement sont dans ce cas. L'option --init de docker run, ou init: true dans Compose, ajoute un petit processus qui relaie les signaux et règle le problème. Pour vérifier, chronométrez un docker stop : s'il dure 10 secondes, l'arrêt n'est pas propre.
docker initPose les questions sur votre projet et génère un Dockerfile, un .dockerignore, un compose.yaml et un README.Docker.md de départ.
AttentionIl propose d'écraser les fichiers existants, sans retour possible.
09Docker Compose
Une application réelle a souvent plusieurs morceaux : le site, la base, parfois un cache. Plutôt que trois docker run à rallonge, Compose décrit l'ensemble dans un fichier compose.yaml et le lance d'une commande.
services:
web:
build: .
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:${POSTGRES_PASSWORD}@db:5432/app
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17-alpine
environment:
POSTGRES_USER: app
POSTGRES_DB: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
db-data:Compose lit ${POSTGRES_PASSWORD} dans le fichier .env placé à côté. Le service db n'a pas de ports: : la base reste invisible de l'extérieur. Si un vieux tutoriel commence par une ligne version:, supprimez-la. Compose l'ignore et affiche un avertissement qui la signale comme obsolète.
Compose crée un réseau nommé <projet>_default, le projet prenant par défaut le nom du dossier. Chaque service y est joignable par son nom : l'application parle à db:5432, sans adresse IP. Entre services, on utilise le port du conteneur. Le port de l'hôte ne sert qu'aux connexions venues de l'extérieur.
La clé expose rend un port accessible aux autres services sans le publier sur la machine. La clé ports, elle, le publie, avec la même mise en garde que -p sur un serveur.
depends_on règle l'ordre de démarrage. Sous sa forme courte, Compose attend seulement que la base soit lancée, pas qu'elle accepte des connexions. Pour attendre qu'elle soit prête, il faut un healthcheck sur la base et la condition service_healthy, comme dans l'exemple. Les trois conditions possibles :
| Condition | Compose attend que la dépendance |
|---|---|
service_started | soit démarrée, ce que fait la forme courte |
service_healthy | passe son healthcheck |
service_completed_successfully | se termine sans erreur, utile pour une migration de base |
Sans réglage, un healthcheck tourne toutes les 30 secondes, abandonne une vérification après 30 secondes et déclare le service en échec après 3 tentatives ratées. Pour une base qui démarre en quelques secondes, des intervalles de 5 secondes évitent d'attendre une demi-minute.
restart dit quoi faire quand un conteneur s'arrête. no, la valeur par défaut, ne relance jamais. always relance toujours. on-failure relance après une sortie en erreur. unless-stopped relance sauf si vous avez arrêté le service vous-même, ce qui en fait le bon choix pour un serveur.
docker compose up -dCrée et démarre tous les services en arrière-plan. Relancée après une modification de compose.yaml, elle recrée les services concernés.
docker compose up -d --buildReconstruit les images avant de démarrer.
QuandAprès une modification du code ou du Dockerfile.
docker compose psListe les services du projet et leur état, healthcheck compris.
docker compose logs -f webSuit le journal d'un service.
docker compose exec web shOuvre un shell dans le service.
docker compose restart webRedémarre un service. Les changements de compose.yaml ne sont pas pris en compte : pour eux, c'est up -d.
docker compose configAffiche le fichier final, variables remplacées. Une faute de syntaxe ou une variable vide s'y repère vite.
docker compose downArrête et supprime les conteneurs et le réseau. Les volumes restent.
docker compose down -vSupprime aussi les volumes nommés déclarés dans le fichier et les volumes anonymes. La base repart de zéro.
10Une vraie appli : Umami
Pour mettre tout cela en pratique, lancez Umami, un outil libre de mesure d'audience, sous licence MIT. Son dépôt fournit un fichier Compose prêt à l'emploi. La dernière version publiée est la 3.4.1, sortie le 23 octobre 2026.
mkdir umami && cd umami curl -O https://raw.githubusercontent.com/umami-software/umami/master/docker-compose.yml docker compose up -d
Au premier lancement, Docker télécharge les images, puis Umami crée ses tables. Ouvrez http://localhost:3000 et connectez-vous avec le compte créé par défaut, admin et le mot de passe umami. Changez-le aussitôt. Si Docker répond address already in use, un autre programme occupe déjà le port 3000.
Le fichier contient deux services, et vous savez maintenant le lire :
- umami utilise l'image
ghcr.io/umami-software/umami:latest, publie le port 3000 et reçoit ses réglages parDATABASE_URL,APP_SECRETetTWO_FACTOR_ENCRYPTION_KEY. Il attend que la base soit saine, activeinit: trueetrestart: always, et se vérifie en appelant/api/heartbeat. - db tourne sur
postgres:15-alpine, range ses données dans le volumeumami-db-dataet se vérifie avecpg_isready. Le double dollar de$${POSTGRES_USER}empêche Compose de remplacer la variable, qui est lue dans le conteneur.
Avant de mettre ce fichier sur un serveur ouvert à internet, changez quatre choses.
- Épinglez la version :
ghcr.io/umami-software/umami:3.4.1au lieu delatest. - Générez les deux secrets,
APP_SECRETetTWO_FACTOR_ENCRYPTION_KEY, avecopenssl rand -hex 32. Le fichier livré contient des valeurs d'exemple. - Sortez les mots de passe de la base dans un fichier
.env, lu par${…}. - Limitez le port à la machine avec
"127.0.0.1:3000:3000", et placez devant un reverse proxy comme Caddy ou nginx, qui gère le HTTPS.
Le piège du mot de passe Postgres. L'image ne lit POSTGRES_PASSWORD qu'au tout premier démarrage, quand le volume est vide. Changer la variable ensuite ne change rien, et l'application se heurte à password authentication failed. Fixez le mot de passe avant le premier lancement, ou changez-le dans la base elle-même.
Pour mesurer un site, ajoutez-le dans Umami, puis copiez le code de suivi qu'il fournit. Il ressemble à ceci :
<script defer src="https://[votre-domaine-umami]/script.js" data-website-id="[identifiant-du-site]"></script>
Sur un site WordPress, la balise se place dans l'en-tête, par le thème enfant ou une extension d'insertion de code. En production, Umami doit tourner en continu sur un serveur, derrière un nom de domaine en HTTPS : une page en HTTPS refuse de charger un script servi en HTTP. Les bloqueurs de publicité arrêtent une partie des scripts de mesure, alors lisez les chiffres comme un minimum.
Et Kubernetes ? Il répartit des conteneurs sur un groupe de machines, avec bascule automatique et montée en charge. Pour un site, une base et quelques services sur un seul serveur, Compose suffit largement.
11Les pièges
- Un FROM sans version. Le build change le jour où
latestbouge. - Un mot de passe dans le Dockerfile. Il reste lisible dans l'image, même supprimé plus loin.
- Corriger un conteneur à la main avec exec. La correction disparaît au prochain conteneur. Corrigez le Dockerfile.
- Une base sans volume. Les données partent avec le premier
docker rm. - Publier le port de la base sur un serveur. Docker passe devant ufw, la base se retrouve sur internet.
- Une application qui écoute sur localhost. Le port publié ne répond pas, écoutez sur
0.0.0.0. - Croire que depends_on attend que la base soit prête. Il faut un healthcheck et
service_healthy. - Taper down -v par habitude. Les volumes et leurs données partent avec.
12L'antisèche
| Vous voulez | Tapez |
|---|---|
| Lancer un conteneur | docker run -d -p 8080:80 --name web image:tag |
| Voir ce qui tourne | docker ps, ou docker ps -a avec les arrêtés |
| Lire le journal | docker logs -f --tail 100 web |
| Entrer dans un conteneur | docker exec -it web sh |
| Arrêter et supprimer | docker stop web puis docker rm web |
| Garder des données | -v pg-data:/chemin/des/donnees |
| Construire une image | docker build -t mon-appli:1.0 . |
| Voir ses couches | docker history mon-appli:1.0 |
| Mesurer la place prise | docker system df |
| Faire le ménage | docker system prune |
| Lancer un projet Compose | docker compose up -d |
| Appliquer un changement de code | docker compose up -d --build |
| Vérifier le fichier Compose | docker compose config |
| Tout arrêter, données gardées | docker compose down |
| Démarrer un Dockerfile | docker init, puis la grille de relecture |
Toutes les options sont décrites dans la référence officielle de Docker. Pour le fichier Compose, tout est détaillé dans la référence du format Compose, clé par clé.
Les autres ressources
Envie d'un parcours structuré ?
Ces ressources montrent notre façon de travailler. Pour apprendre étape par étape et appliquer à vos propres projets, Flow Buster propose des formations professionnelles à Bordeaux, en présentiel ou à distance.