← Retour aux tutoriels

Docker Compose 101 : orchestrer des applications multi-conteneurs en local

dockerdocker-composeconteneursdeveloppement-localdebutantorchestrationmicroservicesdevops

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

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 commande docker-compose peut 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 :

Sans Compose, vous enchaînez des docker run avec :

Cela devient fragile et difficile à partager avec une équipe.

Ce que Compose apporte


3) Structure d’un projet Compose

Convention courante :

mon-projet/
├─ compose.yaml
├─ .env
└─ app/
   ├─ Dockerfile
   ├─ package.json
   └─ src/

4) Premier exemple complet : API + PostgreSQL + Redis

Objectif

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).

image vs build

environment

Variables d’environnement passées au conteneur.

volumes

db_data:/var/lib/postgresql/data signifie :

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 :

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 à :

Vérifiez :

docker network ls
docker network inspect mon-projet_default

Points clés

Cas d’usage : isoler des services

Vous pouvez créer plusieurs réseaux, par exemple :

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 :

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 :

  1. Installer les dépendances sur l’hôte (pas idéal si vous voulez un environnement 100% conteneurisé).
  2. 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


10) Dépendances : limites de depends_on et stratégie robuste

Même avec depends_on, une application peut échouer si :

Approche recommandée

  1. Utiliser healthcheck pour les services “infrastructure” (db, redis).
  2. Ajouter une logique de retry côté application (toujours utile).
  3. É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 :

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 :

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 :

depends_on n’est pas un orchestrateur complet

Compose n’est pas Kubernetes. Il est parfait pour :

Mais il ne gère pas :


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 :

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

2) Ne pas exposer inutilement les ports

3) Utiliser des volumes nommés pour les données

4) Prévoir des healthchecks

5) Documenter les commandes usuelles

Ajoutez un README.md avec :


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 :

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.).