Docker Compose 101 : orchestrer des applications multi-conteneurs en local
Docker Compose est l’outil le plus simple et le plus efficace pour décrire, lancer et orchestrer plusieurs conteneurs qui doivent fonctionner ensemble sur une machine locale (poste de développement, VM, serveur de test). Là où docker run devient vite illisible (réseaux, volumes, variables d’environnement, dépendances), Compose permet de déclarer l’architecture d’une application dans un fichier unique et de la gérer avec quelques commandes.
Ce tutoriel va au-delà des bases : vous allez comprendre la logique des réseaux, des volumes, des dépendances, des profils, des bonnes pratiques, et vous aurez des commandes réelles pour diagnostiquer et dépanner.
1) Prérequis et vérifications
Installer Docker et Compose
- Sur Linux : Docker Engine + plugin Compose (souvent
docker-compose-plugin) - Sur macOS/Windows : Docker Desktop inclut Compose
Vérifiez que tout est prêt :
docker version
docker compose version
Remarque : la commande recommandée est
docker compose(avec un espace). L’ancienne commandedocker-composepeut exister selon les installations, mais elle est progressivement remplacée.
2) Pourquoi Docker Compose ?
Problème typique sans Compose
Vous avez une application web qui dépend de :
- une base PostgreSQL
- un cache Redis
- un serveur web (par exemple une API Node.js)
Sans Compose, vous enchaînez des docker run avec :
- des variables d’environnement
- des ports
- des volumes
- un réseau commun
- des liens de dépendance
Cela devient fragile et difficile à partager avec une équipe.
Ce que Compose apporte
- Déclaratif : un fichier
compose.yamldécrit l’ensemble - Reproductible : un
docker compose uprecrée l’environnement - Isolé : réseaux et volumes dédiés au projet
- Lisible : on voit immédiatement les services, ports, variables, volumes
- Évolutif : ajouter un service (ex. adminer, mailhog) est trivial
3) Structure d’un projet Compose
Convention courante :
mon-projet/
├─ compose.yaml
├─ .env
└─ app/
├─ Dockerfile
├─ package.json
└─ src/
compose.yaml: définition des services (conteneurs), réseaux, volumes.env: variables injectées dans Compose (pratique pour éviter de dupliquer)app/Dockerfile: image de votre application
4) Premier exemple complet : API + PostgreSQL + Redis
Objectif
api: application (exemple Node) construite depuis un Dockerfiledb: PostgreSQL avec volume persistantredis: cache Redis- un réseau par défaut géré par Compose
- des healthchecks pour savoir si les services sont prêts
Fichier compose.yaml
Créez compose.yaml à la racine :
services:
db:
image: postgres:16
environment:
POSTGRES_DB: appdb
POSTGRES_USER: appuser
POSTGRES_PASSWORD: secret
volumes:
- db_data:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U appuser -d appdb"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:7
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 20
api:
build:
context: ./app
environment:
DATABASE_URL: postgres://appuser:secret@db:5432/appdb
REDIS_URL: redis://redis:6379
PORT: "3000"
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
volumes:
db_data:
Explications approfondies
services
Chaque entrée sous services décrit un conteneur (ou plutôt un service : Compose peut le redémarrer, le reconstruire, le scaler).
db,redis,apisont des noms logiques. Ils deviennent aussi des noms DNS sur le réseau Compose.
Ainsi, depuisapi, l’hôtedbpointe vers PostgreSQL, etredisvers Redis.
image vs build
image: postgres:16: on utilise une image officielle existante.build: context: ./app: on construit une image locale à partir d’un Dockerfile.
environment
Variables d’environnement passées au conteneur.
- Pour PostgreSQL, elles initialisent la base au premier démarrage.
- Pour l’API, elles donnent les URL de connexion.
Notez@db:5432: pas besoin d’IP, Compose fournit la résolution DNS.
volumes
db_data:/var/lib/postgresql/data signifie :
db_dataest un volume Docker géré par Compose- il est monté dans le conteneur à l’emplacement où PostgreSQL stocke ses données
- résultat : vos données survivent aux redémarrages et recréations de conteneurs
ports
"5432:5432" expose PostgreSQL sur votre machine (hôte).
Format : "PORT_HOTE:PORT_CONTENEUR".
En développement, exposer la base peut être pratique (client SQL local). En environnement plus strict, on évite d’exposer inutilement.
healthcheck
Un conteneur peut être “démarré” mais pas “prêt”.
Le healthcheck permet de savoir quand un service est réellement opérationnel :
- PostgreSQL :
pg_isready - Redis :
redis-cli ping
depends_on avec conditions
depends_on gère l’ordre de démarrage, mais surtout ici, on utilise :
condition: service_healthy
Cela évite que l’API démarre avant que la base et Redis soient prêts.
5) Dockerfile minimal pour l’API (exemple)
Dans app/Dockerfile :
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Exemple de app/package.json (très simplifié) :
{
"name": "api",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"dependencies": {
"pg": "^8.11.3",
"redis": "^4.6.13",
"express": "^4.18.3"
}
}
6) Lancer, arrêter, reconstruire : commandes essentielles
Depuis la racine du projet :
Démarrer en avant-plan
docker compose up
Vous verrez les logs de tous les services.
Démarrer en arrière-plan
docker compose up -d
Voir l’état
docker compose ps
Voir les logs
Tous les services :
docker compose logs -f
Un service en particulier :
docker compose logs -f api
Arrêter
docker compose stop
Arrêter et supprimer les conteneurs (mais garder les volumes)
docker compose down
Tout supprimer, y compris les volumes (attention : perte de données)
docker compose down -v
Reconstruire l’image de l’API
docker compose build api
docker compose up -d --build
7) Réseaux : comprendre la connectivité interne
Par défaut, Compose crée un réseau dédié au projet. Le nom ressemble à :
mon-projet_default
Vérifiez :
docker network ls
docker network inspect mon-projet_default
Points clés
- Tous les services sont sur le même réseau par défaut (sauf configuration contraire).
- Les services se joignent via leur nom de service (
db,redis,api). - Vous n’avez pas besoin d’exposer des ports pour que les conteneurs communiquent entre eux.
Exemple :apipeut joindredb:5432même si vous supprimezportscôtédb.
Lesportsservent à joindre le service depuis l’hôte.
Cas d’usage : isoler des services
Vous pouvez créer plusieurs réseaux, par exemple :
- un réseau “backend” (db + redis + api)
- un réseau “frontend” (api + reverse proxy)
Cela limite l’exposition interne et clarifie l’architecture.
8) Volumes : persistance et partage de fichiers
Volume nommé (recommandé pour bases de données)
Exemple déjà vu :
volumes:
db_data:
Avantages :
- géré par Docker
- performant
- indépendant du chemin local
- portable
Lister les volumes :
docker volume ls
docker volume inspect mon-projet_db_data
Montage de répertoire (bind mount) pour le développement
Pour du “hot reload” (modifier le code local et le voir dans le conteneur), on monte le répertoire :
api:
build:
context: ./app
volumes:
- ./app:/usr/src/app
ports:
- "3000:3000"
Attention aux dépendances Node
Si vous montez tout ./app dans /usr/src/app, vous risquez d’écraser node_modules installé dans l’image. Solutions courantes :
- Installer les dépendances sur l’hôte (pas idéal si vous voulez un environnement 100% conteneurisé).
- Utiliser un volume anonyme pour
node_modules:
api:
volumes:
- ./app:/usr/src/app
- /usr/src/app/node_modules
Ainsi, le code vient de l’hôte, mais node_modules reste géré dans le conteneur.
9) Variables d’environnement et fichier .env
Compose charge automatiquement un fichier .env situé dans le même dossier que compose.yaml.
Créez .env :
POSTGRES_DB=appdb
POSTGRES_USER=appuser
POSTGRES_PASSWORD=secret
API_PORT=3000
Puis adaptez compose.yaml :
services:
db:
image: postgres:16
environment:
POSTGRES_DB: ${POSTGRES_DB}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
api:
build:
context: ./app
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
PORT: "${API_PORT}"
ports:
- "${API_PORT}:3000"
Bonnes pratiques
- Ne mettez pas de secrets sensibles en clair si le dépôt est public.
- Pour des secrets locaux,
.envpeut être ignoré via.gitignore. - Pour des scénarios plus avancés, on utilise des mécanismes de secrets (selon contexte), mais en local
.envest souvent suffisant.
10) Dépendances : limites de depends_on et stratégie robuste
Même avec depends_on, une application peut échouer si :
- elle démarre trop vite
- elle ne gère pas les retries de connexion
- la base met du temps à appliquer des migrations
Approche recommandée
- Utiliser
healthcheckpour les services “infrastructure” (db, redis). - Ajouter une logique de retry côté application (toujours utile).
- Éventuellement, ajouter un service “migrations” séparé.
Exemple d’un service de migrations (conceptuel) :
services:
migrate:
build:
context: ./app
command: ["npm", "run", "migrate"]
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
Puis l’API peut dépendre de migrate si nécessaire (selon votre stratégie).
11) Profils : activer des services optionnels
Les profils permettent de lancer certains services uniquement quand on le souhaite (ex. outils de debug).
Ajoutons Adminer (interface web DB) seulement à la demande :
services:
adminer:
image: adminer:4
profiles: ["debug"]
ports:
- "8080:8080"
depends_on:
db:
condition: service_healthy
Lancer sans le profil :
docker compose up -d
Lancer avec le profil :
docker compose --profile debug up -d
12) Exécuter des commandes dans un conteneur (debug quotidien)
Ouvrir un shell
docker compose exec api sh
Pour PostgreSQL (selon image) :
docker compose exec db bash
Lancer une commande ponctuelle
Exemple : afficher les variables d’environnement dans l’API :
docker compose exec api env | sort
Se connecter à PostgreSQL
Avec psql dans le conteneur db :
docker compose exec db psql -U appuser -d appdb
Tester Redis
docker compose exec redis redis-cli ping
docker compose exec redis redis-cli info | head
13) Gestion des logs et diagnostic
Suivre les logs d’un service
docker compose logs -f db
Voir les dernières lignes
docker compose logs --tail=200 api
Inspecter un conteneur
Récupérer l’identifiant :
docker compose ps
Puis :
docker inspect <ID_OU_NOM_CONTENEUR>
Comprendre un problème de port déjà utilisé
Si 3000 est déjà pris :
- changez le port hôte, par exemple
3001:3000 - ou identifiez le processus qui écoute :
sudo lsof -i :3000
14) Redémarrage automatique et politique de restart
En local, ce n’est pas toujours nécessaire, mais utile si vous voulez un comportement stable.
Exemple :
services:
api:
restart: unless-stopped
Politiques courantes :
no(par défaut)alwayson-failureunless-stopped
En développement, un redémarrage automatique peut masquer des erreurs (boucles de crash). Utilisez-le en connaissance de cause.
15) Santé, readiness, et différences importantes
healthcheck ne remplace pas la robustesse applicative
Un service peut être “healthy” puis tomber. Votre application doit savoir :
- réessayer
- gérer les erreurs
- se reconnecter
depends_on n’est pas un orchestrateur complet
Compose n’est pas Kubernetes. Il est parfait pour :
- local
- CI simple
- environnements de test
Mais il ne gère pas :
- l’auto-scaling avancé
- le scheduling multi-nœuds
- la haute disponibilité
16) Nettoyage : éviter l’accumulation
Supprimer les ressources du projet
docker compose down --remove-orphans
Nettoyer images/volumes inutilisés (global)
Attention : cela peut supprimer des ressources non liées à votre projet si elles ne sont plus utilisées.
docker system df
docker system prune
Pour supprimer aussi les volumes inutilisés :
docker system prune --volumes
17) Exemple plus réaliste : reverse proxy local (Nginx) + API
Un cas fréquent : exposer plusieurs services derrière un reverse proxy, avec un seul port public (ex. 80/8080).
Ajoutez un service nginx :
services:
nginx:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
api:
condition: service_started
api:
build:
context: ./app
environment:
PORT: "3000"
expose:
- "3000"
Notez l’utilisation de expose :
exposerend le port visible aux autres conteneurs (documentation interne)- contrairement à
ports, il n’expose pas sur l’hôte
Exemple de nginx/default.conf :
server {
listen 80;
location / {
proxy_pass http://api:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Démarrez :
docker compose up -d
curl -i http://localhost:8080/
18) Bonnes pratiques de conception Compose (local)
1) Garder compose.yaml lisible
- Nommez clairement les services (
db,redis,api,worker) - Évitez les configurations répétées : utilisez
.envet des valeurs communes
2) Ne pas exposer inutilement les ports
- Exposez seulement ce qui doit être accessible depuis l’hôte
- Gardez la base accessible uniquement en interne si possible
3) Utiliser des volumes nommés pour les données
- Base de données : volume nommé
- Cache : souvent pas nécessaire (selon besoin)
4) Prévoir des healthchecks
- Surtout pour les bases, brokers, caches
5) Documenter les commandes usuelles
Ajoutez un README.md avec :
docker compose up -ddocker compose logs -fdocker compose down -v(avec avertissement)
19) Commandes récapitulatives (mémo)
# Lancer
docker compose up
docker compose up -d
# Reconstruire
docker compose up -d --build
docker compose build --no-cache api
# État et logs
docker compose ps
docker compose logs -f
docker compose logs -f api
# Exécuter dans un conteneur
docker compose exec api sh
docker compose exec db psql -U appuser -d appdb
# Arrêter / supprimer
docker compose stop
docker compose down
docker compose down -v
# Nettoyage global (attention)
docker system prune
docker system prune --volumes
20) Conclusion : ce que vous savez faire maintenant
Avec Docker Compose, vous savez désormais :
- Décrire une application multi-conteneurs (API + DB + cache)
- Comprendre les mécanismes de réseau (DNS par nom de service)
- Gérer la persistance via volumes
- Utiliser
healthchecketdepends_onde manière fiable - Lancer des services optionnels avec des profils
- Diagnostiquer via logs, shells, inspect
- Mettre en place un reverse proxy local
Si vous souhaitez, je peux proposer une variante orientée développement (rechargement à chaud, profils “dev/test”, service de migrations, et séparation des réseaux) ou adapter le tutoriel à votre stack (Python/FastAPI, PHP/Laravel, Java/Spring, etc.).