// Conteneurs & DevOps

Docker Compose par l'exemple : orchestrer ses conteneurs

Lancer un conteneur à la main avec une longue ligne docker run fonctionne pour un service isolé. Mais dès qu’une application combine un serveur web, une base de données et un cache, retaper ces commandes à chaque démarrage devient ingérable. Docker Compose décrit toute la pile dans un seul fichier YAML et la pilote avec des commandes courtes. Ce guide passe en revue la structure du fichier, chaque section utile et plusieurs exemples concrets prêts à copier.

Un premier fichier docker-compose.yml minimal

Compose lit par défaut un fichier nommé docker-compose.yml (ou compose.yml) placé à la racine du projet. Sa clé principale est services : chaque entrée décrit un conteneur.

services:
  web:
    image: nginx:1.27
    ports:
      - "8080:80"

Ce fichier suffit pour lancer un serveur Nginx accessible sur http://localhost:8080. La commande docker compose up construit puis démarre le service, et Compose se charge de créer le réseau et de nommer le conteneur.

La structure d’un service

Un service accepte de nombreuses clés. Voici les plus courantes, regroupées dans un exemple commenté :

services:
  api:
    build: .                 # construire depuis le Dockerfile local
    image: mon-api:latest    # nom de l'image produite
    container_name: mon-api  # nom fixe du conteneur
    ports:
      - "3000:3000"          # hôte:conteneur
    restart: unless-stopped  # redémarrage automatique
    working_dir: /app
    command: node server.js  # remplace la CMD du Dockerfile

Deux clés s’excluent souvent dans la pratique : image seule télécharge une image existante, tandis que build construit à partir d’un Dockerfile. Vous pouvez combiner les deux : build construit, image nomme le résultat. Si vous débutez sur ces notions, notre guide Docker pour débutants reprend images et Dockerfile depuis zéro.

Personnaliser la construction avec build

Quand le Dockerfile n’est pas à la racine ou porte un autre nom, build accepte une forme détaillée :

services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile.prod
      args:
        NODE_ENV: production

Le context est le dossier envoyé au moteur Docker ; dockerfile pointe le fichier de recette ; args transmet des variables au moment du build (les ARG du Dockerfile).

Les variables d’environnement

Trois approches cohabitent, du plus simple au plus propre.

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: production

Pour éviter d’écrire des secrets en clair, on externalise dans un fichier .env (lu automatiquement par Compose) puis on interpole les valeurs :

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
# Fichier .env à la racine du projet
DB_USER=app
DB_PASSWORD=un-mot-de-passe-solide

Troisième option, env_file, injecte tout un fichier de variables dans le conteneur :

services:
  api:
    build: .
    env_file:
      - ./config/api.env

Pensez à ajouter .env à votre .gitignore pour ne jamais versionner de secrets.

Les volumes : persister les données

Sans volume, tout ce qu’un conteneur écrit disparaît à sa suppression. Compose gère deux formes utiles.

services:
  db:
    image: postgres:16
    volumes:
      - donnees-db:/var/lib/postgresql/data   # volume nommé
  web:
    build: .
    volumes:
      - ./src:/app/src                          # bind mount (dev)

volumes:
  donnees-db:

Le volume nommé donnees-db est géré par Docker et survit aux redémarrages : idéal pour une base de données. Le bind mount ./src:/app/src relie un dossier local au conteneur, parfait en développement pour voir ses modifications sans reconstruire. Chaque volume nommé utilisé doit être déclaré dans la section volumes: de haut niveau. Pour approfondir sauvegardes et cas d’usage, voyez notre guide des volumes Docker.

Les networks : faire communiquer les services

Par défaut, Compose crée un réseau unique et y attache tous les services. Ils se joignent alors par leur nom de service : depuis web, l’hôte db résout vers la base. Aucune configuration n’est nécessaire pour ce cas courant.

Pour isoler des groupes de services, on déclare des réseaux explicites :

services:
  web:
    build: .
    networks:
      - frontend
  api:
    build: ./api
    networks:
      - frontend
      - backend
  db:
    image: postgres:16
    networks:
      - backend

networks:
  frontend:
  backend:

Ici web ne peut pas joindre db directement : seul api, présent sur les deux réseaux, fait le pont. C’est une bonne pratique de segmentation.

depends_on : maîtriser l’ordre de démarrage

depends_on impose un ordre : un service ne démarre qu’après ceux dont il dépend.

services:
  api:
    build: .
    depends_on:
      - db
  db:
    image: postgres:16

Attention à une subtilité fréquente : depends_on attend que le conteneur db soit démarré, pas que PostgreSQL soit prêt à accepter des connexions. Pour attendre la disponibilité réelle, on ajoute un healthcheck et une condition :

services:
  db:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5
  api:
    build: .
    depends_on:
      db:
        condition: service_healthy

L’api ne démarre alors que lorsque le healthcheck de db passe au vert.

Exemple complet : une stack web + base + cache

Voici une pile réaliste : une API Node, une base PostgreSQL persistante et un cache Redis.

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://app:${DB_PASSWORD}@db:5432/production
      REDIS_URL: redis://cache:6379
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_started
    restart: unless-stopped

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: production
    volumes:
      - donnees-db:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      timeout: 3s
      retries: 5

  cache:
    image: redis:7-alpine
    restart: unless-stopped

volumes:
  donnees-db:

L’API joint la base via l’hôte db et le cache via cache, grâce au réseau implicite. Les données de PostgreSQL survivent dans le volume donnees-db.

Les commandes à connaître

Compose se pilote avec une poignée de sous-commandes.

# Construire et démarrer toute la pile en arrière-plan
docker compose up -d

# Reconstruire les images avant de démarrer
docker compose up -d --build

# Suivre les logs de tous les services en temps réel
docker compose logs -f

# Logs d'un seul service
docker compose logs -f api

# Lister l'état des services
docker compose ps

# Ouvrir un shell dans un conteneur en cours
docker compose exec api sh

# Arrêter et supprimer conteneurs et réseaux (volumes conservés)
docker compose down

# Tout supprimer, y compris les volumes nommés (destructif)
docker compose down -v

Le drapeau -d (detached) rend la main au terminal ; sans lui, les logs défilent au premier plan. Retenez surtout la différence entre down et down -v : le second efface vos données.

En résumé

Docker Compose transforme une pile multi-conteneurs en un fichier lisible et versionnable. Vous savez maintenant structurer les services, injecter des variables, persister avec des volumes, segmenter avec des networks, ordonner avec depends_on et piloter le tout avec up, down et logs. La suite logique est d’intégrer ce fichier à un pipeline de déploiement pour lancer la même pile en production.

À lire ensuite