Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 819 minIntermédiaire

Docker Compose : écrire un fichier compose.yaml

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire un fichier compose.yaml actuel, sans la clé version ;
  • utiliser les variables et le fichier .env sans exposer de secret ;
  • faire attendre un service que sa base soit prête ;
  • démarrer, mettre à jour et arrêter une application avec Compose.

4 exercices corrigés

Docker Compose est l'outil qui décrit une application de plusieurs conteneurs dans un seul fichier YAML, compose.yaml, et qui la démarre, la met à jour ou l'arrête d'une seule commande (docker compose up -d). Ce chapitre présente la syntaxe actuelle (sans la clé version, devenue obsolète), les clés d'un service, les volumes et les réseaux, les variables d'environnement et le fichier .env, l'ordre de démarrage avec les contrôles de santé, puis les commandes du quotidien.

Pourquoi Compose

Lancer l'API et sa base à la main demande déjà quatre commandes longues : un réseau, un volume, deux docker run avec leurs options. Les retaper à l'identique à chaque mise à jour, sur chaque serveur, est source d'erreurs. Compose remplace ces commandes par un fichier déclaratif : tu décris l'état voulu (quels services, avec quelles images, quels volumes, quels réseaux), et docker compose up -d crée ou recrée ce qui doit l'être. Le fichier se versionne avec le code dans Git : c'est la documentation exacte du déploiement.

Compose aujourd'hui : ce qui a changé

Ancienne pratiquePratique actuelle
Commande docker-compose (avec tiret), écrite en PythonCommande docker compose (avec une espace), intégrée à Docker
Fichier docker-compose.ymlFichier compose.yaml (les anciens noms restent lus)
Première ligne version: "3.8"Pas de clé version : le format suit la Compose Specification, qui évolue en continu
links: pour relier les conteneursLe réseau par défaut du projet et son DNS interne
depends_on: qui n'attend que le lancementdepends_on avec condition: service_healthy

Avec un fichier qui commence encore par version:, Compose avertit :

text
WARN[0000] /srv/gestion/docker-compose.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion

Supprime simplement la ligne : Compose valide toujours le fichier avec le schéma le plus récent, quelle que soit la valeur écrite.

Un premier fichier compose.yaml

Voici l'API sorbier-frais et sa base, pour un environnement de développement :

yaml
# compose.yaml
services:
  api:
    build: .
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      DB_HOST: bdd
      DB_NAME: frais
      DB_USER: frais
      DB_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
    depends_on:
      bdd:
        condition: service_healthy
    restart: unless-stopped

  bdd:
    image: mysql:8.4
    environment:
      MYSQL_DATABASE: frais
      MYSQL_USER: frais
      MYSQL_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:?définis DB_ROOT_PASSWORD dans .env}
    volumes:
      - mysql-donnees:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

volumes:
  mysql-donnees:

Et à côté, le fichier .env, jamais versionné (ajoute-le au .gitignore) :

text
DB_PASSWORD=7uQ2-mVx9-Lr4k-Zp8s
DB_ROOT_PASSWORD=Jw3n-Tb6c-Hq1e-Fy5d

YAML se lit à l'indentation : deux espaces par niveau, jamais de tabulation, et un tiret pour chaque élément de liste.

PartieRôle
services:Les conteneurs de l'application ; chaque clé (api, bdd) est un service
build: .Construit l'image à partir du Dockerfile du dossier courant
image: mysql:8.4Utilise une image existante
ports:Publication de ports ; l'API n'est publiée que sur la boucle locale, pour les tests
environment:Variables du conteneur ; ${…} est remplacé par Compose au moment de lire le fichier
DB_HOST: bddLe nom du service sert de nom d'hôte sur le réseau du projet
depends_on … service_healthyL'API ne démarre que quand le contrôle de santé de la base réussit
volumes: (dans le service)Monte le volume nommé mysql-donnees
volumes: (en bas)Déclare les volumes nommés du projet

Les commandes du quotidien

Toutes les commandes se lancent dans le dossier du compose.yaml :

CommandeEffet
docker compose up -dCrée réseau, volumes et conteneurs, construit les images si besoin, démarre tout en arrière-plan
docker compose psL'état des services, avec la santé
docker compose logs -f apiLes journaux d'un service, en continu
docker compose exec bdd mysql -ufrais -p fraisUne commande dans un service qui tourne
docker compose run --rm api node scripts/migrer.jsUn conteneur ponctuel à partir de la définition d'un service
docker compose build(Re)construit les images des services qui ont un build:
docker compose pullTélécharge les nouvelles versions des images
docker compose restart apiRedémarre un service, sans relire le fichier
docker compose stopArrête les conteneurs sans les supprimer
docker compose downArrête et supprime conteneurs et réseau ; garde les volumes
docker compose down -vSupprime aussi les volumes : les données partent
docker compose configAffiche le fichier final, variables remplacées : le premier réflexe de diagnostic
docker compose lsLes projets Compose de l'hôte
bash
docker compose up -d
text
[+] Running 4/4
 ✔ Network sorbier-frais_default      Created     0.1s
 ✔ Volume sorbier-frais_mysql-donnees Created     0.0s
 ✔ Container sorbier-frais-bdd-1      Healthy    21.4s
 ✔ Container sorbier-frais-api-1      Started    21.6s

Le projet, ses noms et son réseau

Compose regroupe tout sous un nom de projet, par défaut le nom du dossier (sorbier-frais ici), et nomme les objets à partir de lui :

ObjetNom
Réseau par défautsorbier-frais_default
Volume mysql-donneessorbier-frais_mysql-donnees
Conteneurssorbier-frais-api-1, sorbier-frais-bdd-1

Tous les services d'un projet sont branchés sur son réseau par défaut et se joignent par leur nom de service. Fixe le nom avec la clé name: sorbier-frais en tête de fichier, pour qu'il ne dépende pas du dossier. Évite container_name: : un nom fixe empêche de lancer deux copies du projet et n'apporte rien, puisqu'on désigne les services par leur nom dans toutes les commandes docker compose.

Attendre qu'un service soit prêt

depends_on seul ne fait qu'ordonner le lancement : l'API démarre dès que le conteneur de la base est lancé, alors que MySQL met une vingtaine de secondes à initialiser sa base la première fois. L'API tente de se connecter, échoue et s'arrête.

Avec condition: service_healthy, Compose attend que le contrôle de santé de la base réussisse. Le test mysqladmin ping -h 127.0.0.1 passe par TCP : pendant l'initialisation, l'image fait tourner un serveur temporaire qui n'écoute pas le réseau, donc le test échoue tant que le vrai serveur n'est pas prêt. mysqladmin ping renvoie 0 dès que le serveur répond, même s'il refuse l'authentification : aucun mot de passe n'est nécessaire dans le test.

ConditionCompose attend que la dépendance…
service_started (par défaut)soit lancée
service_healthysoit healthy
service_completed_successfullyse termine avec le code 0 (une migration, par exemple)

Variables, .env et env_file

Compose lit le fichier .env du dossier du projet et s'en sert pour remplacer les ${VAR} du compose.yaml. Ce fichier ne passe pas tel quel dans les conteneurs : seules les variables listées sous environment: y arrivent.

ÉcritureRésultat
${DB_PASSWORD}La valeur, ou une chaîne vide (avec un avertissement) si elle manque
${TZ:-Europe/Paris}La valeur, ou Europe/Paris si elle est vide ou absente
${DB_PASSWORD:?message}La valeur, ou un arrêt avec ce message si elle est vide ou absente

La clé env_file: est différente : elle charge un fichier de variables directement dans le conteneur, sans passer par le remplacement. Elle sert pour une longue liste de réglages applicatifs.

yaml
services:
  api:
    env_file: config/api.env

Vérifie toujours le résultat avant de lancer :

bash
docker compose config

La commande affiche le fichier complet, variables remplacées, ou l'erreur exacte (indentation, variable manquante).

Les autres clés utiles

CléRôleExemple
command:Remplace le CMD de l'imagecommand: ["node", "src/worker.js"]
user:Utilisateur du processususer: "1000:1000"
networks:Réseaux du service (et déclaration en bas de fichier)Voir le chapitre 9
secrets:Secrets montés dans /run/secrets/Voir le chapitre 11
deploy.resources.limitsLimites de mémoire et de processeurmemory: 512M, cpus: "1.0"
read_only:, tmpfs:Système de fichiers en lecture seule, dossiers temporaires en mémoireread_only: true
logging:Pilote et options de journalisation du servicedriver: local
profiles:Service lancé seulement avec --profileprofiles: ["outils"]
labels:Étiquettes, lues par certains outilscom.sorbier.equipe: "dev"

Plusieurs fichiers. Compose fusionne automatiquement compose.yaml et compose.override.yaml s'il existe. On garde ainsi un fichier commun et un fichier de surcharge pour le développement (ports publiés, montage du code). En production, on précise les fichiers : docker compose -f compose.yaml -f compose.prod.yaml up -d.

Compose Watch. En développement, la section develop.watch d'un service synchronise les fichiers modifiés dans le conteneur, ou reconstruit l'image, pendant que docker compose watch tourne : une alternative au montage du code.

Mettre à jour une application

bash
docker compose pull          # nouvelles images (services avec image:)
docker compose build         # nouvelles images construites (services avec build:)
docker compose up -d         # recrée uniquement les services dont l'image ou la config a changé
docker image prune           # supprime les anciennes images sans étiquette

up -d compare l'état voulu à l'état réel : un service dont rien n'a changé n'est pas touché. Le volume de la base est conservé, puisqu'il n'est pas supprimé par la recréation des conteneurs.

Exercices

Exercice 1 sur 4 Moderniser un ancien fichier Réussi À revoir

Ce fichier docker-compose.yml date de 2019. Liste ce qui est obsolète ou fragile, puis réécris-le en compose.yaml actuel.

yaml
version: "3.7"
services:
  web:
    build: .
    container_name: web
    ports:
      - "80:3000"
    links:
      - db
    depends_on:
      - db
    environment:
      - DB_HOST=db
      - DB_PASSWORD=secret123
  db:
    image: mysql
    environment:
      - MYSQL_ROOT_PASSWORD=secret123
Voir la réponseMasquer la réponse de l'exercice 1

Problèmes :

  1. version: "3.7" est obsolète : Compose l'ignore et affiche un avertissement.
  2. links: est inutile : les services se joignent par leur nom sur le réseau du projet.
  3. depends_on: - db n'attend que le lancement de MySQL, pas qu'il soit prêt.
  4. image: mysql sans étiquette prend latest : version imprévisible.
  5. Aucun volume : les données partent avec le conteneur.
  6. Mots de passe en clair dans le fichier versionné.
  7. container_name: fige les noms sans utilité.
yaml
services:
  web:
    build: .
    ports:
      - "80:3000"
    environment:
      DB_HOST: db
      DB_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD:?définis DB_PASSWORD dans .env}
    volumes:
      - db-donnees:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "--silent"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

volumes:
  db-donnees:

Encore mieux : un compte MySQL dédié à l'application plutôt que root (MYSQL_USER, MYSQL_PASSWORD), et l'application derrière un reverse proxy HTTPS plutôt que publiée en HTTP sur le port 80 (chapitre 9).

Tu avais trouvé ?

Exercice 2 sur 4 Traduire des commandes en Compose Réussi À revoir

Traduis ces commandes en un fichier compose.yaml équivalent, avec le nom de projet wiki.

bash
docker network create wiki_default
docker volume create wiki_pages
docker run -d --name wiki-app-1 --network wiki_default \
  -p 127.0.0.1:8081:80 -v wiki_pages:/var/www/html/data \
  -e TZ=Europe/Paris --restart unless-stopped \
  ghcr.io/sorbier/wiki:1.4
Voir la réponseMasquer la réponse de l'exercice 2
yaml
name: wiki

services:
  app:
    image: ghcr.io/sorbier/wiki:1.4
    ports:
      - "127.0.0.1:8081:80"
    volumes:
      - pages:/var/www/html/data
    environment:
      TZ: Europe/Paris
    restart: unless-stopped

volumes:
  pages:

Compose crée lui-même le réseau wiki_default, le volume wiki_pages et le conteneur wiki-app-1 : les noms des commandes d'origine sont exactement ceux qu'il produit pour le projet wiki, le service app et le volume pages.

Tu avais trouvé ?

Exercice 3 sur 4 Les variables Réussi À revoir

Le fichier .env contient TZ= (vide) et ne contient pas DB_PASSWORD. Que devient chaque ligne au moment de docker compose up ?

  1. TZ: ${TZ:-Europe/Paris}
  2. TZ: ${TZ-Europe/Paris}
  3. DB_PASSWORD: ${DB_PASSWORD}
  4. DB_PASSWORD: ${DB_PASSWORD:?mot de passe manquant}
Voir la réponseMasquer la réponse de l'exercice 3
  1. Europe/Paris : avec :-, la valeur par défaut s'applique si la variable est vide ou absente. Ici elle est vide.
  2. Une chaîne vide : sans les deux-points, la valeur par défaut ne s'applique que si la variable est absente. Elle existe (vide), donc on garde le vide.
  3. Une chaîne vide, et Compose affiche un avertissement : The "DB_PASSWORD" variable is not set. Defaulting to a blank string.
  4. Compose refuse de démarrer et affiche le message mot de passe manquant. C'est la forme à préférer pour un secret : mieux vaut un arrêt net qu'une base démarrée avec un mot de passe vide.

Tu avais trouvé ?

Exercice 4 sur 4 down ou down -v ? Réussi À revoir

Pour chaque situation, choisis entre docker compose stop, docker compose down et docker compose down -v.

  1. Tu changes le port publié de l'API dans compose.yaml et tu veux appliquer le changement.
  2. Tu veux libérer la mémoire de srv-gestion pendant une heure de maintenance, puis tout relancer à l'identique.
  3. En développement, ta base de test est corrompue et tu veux repartir d'une base vide.
  4. L'application est retirée du serveur, après un export de la base archivé ailleurs.
Voir la réponseMasquer la réponse de l'exercice 4
  1. Aucune des trois n'est nécessaire : docker compose up -d suffit, il recrée le service modifié. Un down suivi d'un up -d fonctionne aussi, avec une coupure plus longue.
  2. docker compose stop : les conteneurs sont arrêtés mais gardés, docker compose start les relance tels quels. (down conviendrait aussi : les volumes sont gardés et up -d recrée les conteneurs.)
  3. docker compose down -v : la suppression du volume est voulue, la base sera réinitialisée au prochain up.
  4. docker compose down -v, après avoir vérifié que l'export est lisible et archivé hors du serveur.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Déployer une application complète derrière un reverse proxy HTTPSRevoir le sommaire du cours