Module 05 · Docker DevOps · Chapitre 7 · strict

7. Docker Compose - strict



7.1 a - Présentation

Docker Compose est un outil qui permet d'exécuter un ensemble de conteneurs Docker en même temps. Si une application doit utiliser plusieurs conteneurs, lancer tous ces conteneurs d'un coup est très pratique. Une des utilisations courantes de Docker Compose est de créer des environnements de test : par exemple, si l'on doit tester un script qui doit interagir avec une base de données, on peut lancer un ensemble de conteneurs qui constitueront l'ensemble de test. En particulier, Docker Compose n'est pas utilisé pour créer un ensemble de conteneurs sur différentes machines, mais sur une seule machine. Docker Compose utilise un fichier appelé, par défaut, docker-compose.yaml afin de décrire l'ensemble des éléments à déployer.

Si vous utilisez la machine virtuelle proposée, Docker Compose est déjà installé. Si vous souhaitez installer Docker Compose sur votre machine pour suivre le cours, vous pouvez suivre les instructions disponibles ici.

7.2 b - Comprendre les mots-clés dans un fichier docker-compose.yaml

Le fichier docker-compose.yaml est utilisé pour décrire une application multi-conteneurs dans Docker Compose. Il permet de spécifier les services, les volumes, les réseaux et diverses configurations nécessaires à l'orchestration des conteneurs.

Docker Compose utilise un fichier YAML (docker-compose.yml) pour définir les services, les réseaux et les volumes qui composent votre application. Sa structure est facile à comprendre et hautement configurable, vous permettant de gérer plusieurs conteneurs avec un seul fichier.

Voici un aperçu des composants de base d'un fichier docker-compose.yml :

services : La section services vous permet de définir chaque conteneur qui fera partie de votre application. Chaque service est un conteneur que vous configurez avec des paramètres tels que l'image, les options de build, les variables d'environnement, les ports, etc.

Un exemple de définition d'un service Web de base :

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

Dans ce cas, nous utilisons une image existante (nginx) de Docker Hub et exposons le port 80 du conteneur au port 8080 sur la machine hôte.

networks : Par défaut, Docker Compose crée un réseau de type bridge pour la communication entre tous les services. Cependant, vous pouvez définir des réseaux personnalisés pour mieux contrôler la communication entre les services.

networks:
  my_network:
    driver: bridge

Une fois qu'un réseau est défini, vous pouvez attribuer des services à ce réseau pour une meilleure isolation et un meilleur contrôle.

Volumes : La section « volumes » permet de créer et de gérer un stockage persistant indépendant du cycle de vie du conteneur. Ceci est utile lorsque vous devez conserver des données après plusieurs redémarrages du conteneur.

volumes:
  my_volume:

Vous pouvez ensuite attacher ce volume à un répertoire au sein d'un conteneur pour conserver les données :

services:
  db:
    image: postgres
    volumes:
      - my_volume:/var/lib/postgresql/data

Dans cet exemple, les données de la base de données Postgres sont stockées dans le volume my_volume, garantissant que les données ne sont pas perdues lorsque le conteneur s'arrête ou redémarre.

Grâce à ces composants de base, vous pouvez déjà commencer à définir une application multi-conteneurs. La flexibilité de Docker Compose facilite la mise à l'échelle et la gestion des services à mesure que votre projet se développe.

Chaque service est défini dans la section services correspondante du fichier docker-compose.yml. La configuration la plus basique d'un service comprend la spécification d'une image ou d'une option de build, les ports à exposer et toute configuration supplémentaire spécifique au service, comme les volumes, les réseaux ou les variables d'environnement.

Nous avons ici un exemple de configuration simple avec un serveur Web et un service de base de données :

services:
  web:
    image: nginx:1.18.2
    ports:
      - "8080:80"
    networks:
      - app_network

  db:
    image: postgres:13
    environment:
      POSTGRES_USER: liora
      POSTGRES_PASSWORD: secret
    volumes:
      - db_data:/var/lib/postgresql/data
    networks:
      - app_network

Options de configuration :

image: nginx:latest
ports:
  - "8080:80"

Dans ce cas, le port 80 à l'intérieur du conteneur est mappé (connecté) au port 8080 sur la machine hôte.

environment:
  POSTGRES_USER: admin
  POSTGRES_PASSWORD: secret
volumes:
  - db_data:/var/lib/postgresql/data
networks:
  - app_network

Dépendances de service : Parfois, un service dépend d'un autre. Par exemple, dans une application web, le serveur web peut dépendre de la disponibilité de la base de données. Docker Compose permet de définir des dépendances entre services grâce à l'option « depends_on ».

services:
  web:
    image: nginx:latest
    depends_on:
      - db
    ports:
      - "8080:80"

  db:
    image: postgres:13
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret

Dans cette configuration, Docker Compose garantit que le service de base de données démarre avant le service Web.

Grâce à ces options de configuration de service, vous disposez des éléments de base pour définir l'architecture de votre application. Docker Compose simplifie la gestion du cycle de vie de chaque service et de ses relations avec les autres services de la pile.

Utilisation d'un fichier .env :

Une pratique plus courante consiste à séparer les variables d'environnement du fichier docker-compose.yml à l'aide d'un fichier .env. Ce fichier contient des paires clé-valeur et permet une gestion plus claire des variables d'environnement. Docker Compose chargera automatiquement le fichier .env s'il se trouve dans le même répertoire que le fichier docker-compose.yml.

Exemple de fichier .env :

APP_ENV=production
APP_DEBUG=false
DATABASE_URL=postgres://admin:secret@db:5432/mydb

Dans le fichier docker-compose.yml, vous pouvez référencer ces variables de la façon suivante :

services:
  app:
    image: myapp:latest
    environment:
      - APP_ENV
      - APP_DEBUG
      - DATABASE_URL

Docker Compose remplacera automatiquement les valeurs du fichier .env lors du démarrage des services.

Utilisation des fichiers d'environnement avec l'option « env_file »_ : Vous pouvez également charger explicitement des variables d'environnement depuis un fichier en utilisant l'option « envfile » correspondante dans le fichier docker-compose.yml. Ceci est utile lorsque vous souhaitez charger des variables depuis plusieurs fichiers ou disposer de fichiers distincts

pour différents environnements (par exemple, .env.development, .env.production).

services:
  app:
    image: myapp:latest
    env_file:
      - .env

Vous pouvez également spécifier plusieurs fichiers d'environnement :

services:
  app:
    image: myapp:latest
    env_file:
      - .env
      - .env.liora

Remplacement des variables d'environnement : Si vous définissez des variables d'environnement dans le fichier docker-compose.yml et dans le fichier .env, les variables du fichier docker-compose.yml seront prioritaires. Cela vous permet d'avoir des valeurs par défaut dans votre fichier .env tout en les remplaçant service par service.

services:
  app:
    image: myapp:latest
    environment:
      - APP_ENV=development  # Overrides the value from .env

Exemple : configuration d'un service de base de données avec des variables d'environnement Voici un exemple de configuration de service de base de données utilisant des variables d'environnement :

services:
  db:
    image: postgres:13
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}

Dans le fichier .env :

POSTGRES_USER=admin
POSTGRES_PASSWORD=secret
POSTGRES_DB=my_database

Dans cette configuration, le service Postgres utilisera les identifiants définis dans le fichier .env. Cette configuration garantit que les informations sensibles, comme les mots de passe, ne sont pas codées en dur dans le fichier docker-compose.yml.

7.2.1 Définition des réseaux personnalisés :

Bien que le réseau par défaut fonctionne dans la plupart des cas, vous pourriez souhaiter avoir davantage de contrôle sur la communication de vos services, notamment dans les configurations plus vastes ou complexes. Vous pouvez créer des réseaux personnalisés pour organiser la communication entre vos services ou pour isoler certains services des autres.

Pour définir un réseau personnalisé, utilisez la clé networks dans votre fichier docker-compose.yml et attribuez ensuite des services à ces réseaux de la façon suivante :

services:
  web:
    image: nginx
    networks:
      - frontend_network
      - backend_network

  db:
    image: postgres
    networks:
      - backend_network

networks:
  frontend_network:
  backend_network:

Dans cet exemple, le service web est rattaché à frontend_network et backend_network, ce qui lui permet de communiquer avec les services frontend et backend. Le service de base de données est rattaché uniquement à backend_network, ce qui limite son exposition aux services internes.

7.2.2 Exposer les ports

Docker Compose permet d'exposer les ports des conteneurs à la machine hôte, rendant ainsi les services accessibles depuis l'extérieur du réseau Docker. Cela s'effectue généralement via l'option ports :

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

Dans cet exemple, le port 80 du conteneur Web est mappé au port 8080 de l'hôte. Le service Web est ainsi accessible depuis http://localhost:8080 sur votre machine.

7.2.3 Définition des volumes dans Docker Compose :

Vous pouvez définir des volumes dans le fichier docker-compose.yml sous la section volumes. Les volumes peuvent être nommés ou anonymes. Les volumes nommés ont des noms explicites et peuvent être réutilisés dans plusieurs services, tandis que les volumes anonymes sont générés automatiquement par Docker et n'ont pas de nom spécifique.

Voici comment définir un volume nommé :

volumes:
  db_data:

Dans cet exemple, nous avons défini un volume nommé db_data qui peut être partagé entre les services.

7.2.4 Attacher des volumes aux services

Une fois un volume défini, vous pouvez l'attacher à un service grâce à l'option « volumes » sous ce service. Cela mappe un répertoire de l'hôte à un répertoire du conteneur.

Voici un exemple de connexion du volume db_data à un service PostgreSQL pour conserver les données de la base de données :

services:
  db:
    image: postgres:13
    volumes:
      - db_data:/var/lib/postgresql/data

Dans ce cas, le volume db_data est mappé à /var/lib/postgresql/data à l'intérieur du conteneur, garantissant que toutes les données stockées par PostgreSQL sont enregistrées en dehors du conteneur.

7.2.5 Lier les montages et les volumes

Il existe deux façons de conserver des données dans Docker : les volumes et les montages liés. Bien que les volumes soient gérés par Docker et constituent l'approche recommandée, les montages liés vous permettent de mapper directement les répertoires de votre machine hôte aux répertoires du conteneur.

Voici comment utiliser un montage lié dans Docker Compose :

services:
  app:
    image: myapp:latest
    volumes:
      - ./app:/usr/src/app

Dans cet exemple, le répertoire local ./app de l'hôte est monté dans /usr/src/app à l'intérieur du conteneur. Ceci est particulièrement utile dans les environnements de développement où vous souhaitez refléter les modifications en temps réel.

7.2.6 Partage des volumes entre les services

Parfois, plusieurs services ont besoin d'accéder aux mêmes données. Docker Compose permet de partager des volumes entre services, leur permettant ainsi de collaborer sur les mêmes fichiers ou ensembles de données.

Par exemple, voici comment vous pouvez partager un volume entre un service Web et un service de travail en arrière-plan :

services:
  web:
    image: nginx:latest
    volumes:
      - shared_data:/usr/share/nginx/html

  worker:
    image: myworker:latest
    volumes:
      - shared_data:/usr/src/app/data

volumes:
  shared_data:

Dans cette configuration, les services Web et Worker ont tous deux accès au volume shared_data. Le service Web stocke son contenu statique dans le volume, tandis que le service Worker lit ou traite les mêmes données.

7.2.7 Persistance des données pour les bases de données

Pour les bases de données, l'utilisation de volumes est essentielle pour garantir la conservation des données lors de l'arrêt ou de la suppression des conteneurs. Voici un exemple de configuration Docker Compose pour un service MySQL qui conserve les données à l'aide d'un volume nommé :

services:
  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: mydatabase
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

Dans cette configuration, le volume mysql_data est utilisé pour conserver les données MySQL dans /var/lib/mysql. Cela garantit que même si le conteneur MySQL est arrêté ou recréé, les données de la base de données restent intactes.

7.2.8 Mise à l'échelle des services avec Docker Compose

L'une des fonctionnalités les plus utiles de Docker Compose est la possibilité de faire évoluer vos services horizontalement. Cela signifie que vous pouvez exécuter plusieurs instances d'un service pour gérer davantage de charge ou assurer la redondance. La mise à l'échelle est particulièrement avantageuse pour les services sans état, comme les serveurs web ou les processus de travail.

Vous pouvez faire évoluer les services en utilisant l'option --scale avec la commande docker compose up :

docker compose up --scale web=3

Cette commande démarrera trois instances du service Web. Pour assurer un équilibrage de charge correct entre les services mis à l'échelle, vous devrez peut-être configurer un équilibreur de charge (comme NGINX) ou utiliser la résolution DNS interne de type round-robin de Docker.

Alternativement, vous pouvez définir des répliques de service dans votre docker-compose.yml :

services:
  web:
    image: nginx:latest
    deploy:
      replicas: 3

7.2.9 Dépendances de service avec depends_on

Dans de nombreuses applications, certains services dépendent de la disponibilité d'autres services avant de pouvoir démarrer. Docker Compose propose l'option depends_on pour exprimer cette relation. Cela garantit que Docker démarre les services dépendants dans le bon ordre.

Voici un exemple de service Web qui dépend d'un service de base de données :

services:
  web:
    image: nginx
    depends_on:
      - db

  db:
    image: postgres

Notez toutefois que depends_on contrôle uniquement l'ordre de démarrage ; il n'attend pas que le service dépendant soit prêt (par exemple, que la base de données accepte les connexions). Pour une gestion plus robuste des dépendances, envisagez d'utiliser des contrôles de santé (abordés ci-dessous) ou une logique de nouvelle tentative personnalisée dans votre application.

7.2.10 Contrôles de santé pour garantir la disponibilité du service

Vous pouvez également ajouter des contrôles de santé pour vous assurer que vos services sont bien disponibles avant de permettre à Docker de les utiliser ou de les mettre à l'échelle. Les contrôles de santé permettent à Docker de surveiller la santé de vos services et de garantir qu'ils sont dans un état fonctionnel avant d'être considérés comme prêts.

Voici un exemple d'ajout d'un contrôle de santé pour une base de données MySQL dans Docker Compose :

services:
  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: mydatabase
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      retries: 3
      start_period: 10s
      timeout: 5s

Dans cet exemple, le contrôle de santé utilise la commande mysqladmin ping pour vérifier que la base de données MySQL est opérationnelle. Docker attendra que la base de données soit prête avant de marquer le service comme « sain ».

7.2.11 Gestion des contraintes de ressources

Docker Compose permet de contrôler les ressources (CPU et mémoire) allouées à chaque service. Cela est particulièrement important lors de l'exécution de plusieurs conteneurs sur le même hôte, car cela permet d'éviter les conflits de ressources.

Voici comment définir les limites de ressources pour un service :

services:
  web:
    image: nginx
    deploy:
      resources:
        limits:
          cpus: "0.5"
          memory: "512M"

Dans cet exemple, le service Web est limité à 50 % du processeur (0.5 CPU) et à 512 Mo de mémoire. Vous pouvez également définir des valeurs de réservation pour garantir une certaine quantité de ressources pour un conteneur, ce qui permet de mieux contrôler la gestion des ressources.

7.2.12 Utilisation des politiques de redémarrage pour la résilience des services

Pour garantir le redémarrage automatique de vos services en cas de panne, vous pouvez définir une politique de redémarrage. Docker Compose propose plusieurs options pour gérer le redémarrage des conteneurs :

Voici un exemple d'utilisation d'une politique de redémarrage pour un service Web :

services:
  web:
    image: nginx
    restart: always

Dans ce cas, le service Web sera toujours redémarré s'il plante ou s'il est arrêté involontairement.

7.3 c - Mise en pratique

Docker Compose a été installé sur la machine virtuelle. Pour créer une application Docker Compose, on doit éditer un fichier yaml.

Dans un fichier docker-compose.yml, collez les lignes suivantes:

services:
  jupyter:
    image: jupyter/minimal-notebook:ubuntu-18.04
  elasticsearch:
    image: elasticsearch:7.2.0

Sauvegardez le fichier, quittez-le et exécutez la commande suivante

docker compose up

Dans une autre console, listez les containers en fonctionnement

On peut voir que nous avons pu créer un container: une instance de jupyter/minimal-notebook. Dans le fichier docker-compose.yml, on peut voir deux services jupyter et elasticsearch: ce fichier est en réalité censé lancer deux containers mais comme nous l'avons vu précédemment, les images d'ElasticSearch nécessitent que l'on définissent une variable d'environnement. On peut préciser des variables d'environnement en utilisant le mot-clef environment.

On peut arrêter les containers lancés par Docker Compose en utilisant la ctrl + C dans la console dans laquelle docker compose up a été effectuée ou en tapant docker compose down dans une autre console.

Arrêtez les containers lancés par Docker Compose

Remplacez le contenu du fichier docker-compose.yml par les lignes suivantes

services:
  jupyter:
    image: jupyter/minimal-notebook:ubuntu-18.04
  elasticsearch:
    image: elasticsearch:7.2.0
    environment:
      discovery.type: single-node

Quittez ce fichier, exécutez la commande docker compose up puis listez les containers en fonctionnement

Cette fois-ci on peut voir que les containers sont bien tous les deux en fonctionnement. Pour pouvoir utiliser jupyter, on doit faire suivre le port 8888 du container vers un des ports de la machine hôte.

Remplacez le contenu du fichier docker-compose.yml par ces lignes, puis relancez les services Docker Compose

services:
  jupyter:
    image: jupyter/minimal-notebook:ubuntu-18.04
    ports:
      - target: 8888
        published: 4444
        protocol: tcp
        mode: host
  elasticsearch:
    image: elasticsearch:7.2.0
    environment:
      discovery.type: single-node

Ici on fait suivre le port 8888 vers le port 4444 de la machine hôte. Vous pouvez ouvrir ce port pour arriver sur l'interface graphique de jupyter. On peut noter qu'il existe une autre manière de faire suivre des ports. Par exemple, si on veut faire suivre les ports 9200 et 9300 du container elasticsearch vers ceux de la machine hôte, on peut faire:

services:
  jupyter:
    image: jupyter/minimal-notebook:ubuntu-18.04
    ports:
      - target: 8888
        published: 4444
        protocol: tcp
        mode: host
  elasticsearch:
    image: elasticsearch:7.2.0
    environment:
      discovery.type: single-node
    ports:
      - "9200:9200"
      - "9300:9300"

On peut aussi spécifier les networks ou donner un nom aux containers. Dans le code suivant, on place les containers sur le réseau my_network_from_compose, on les renomme my_jupyter_from_compose et my_es_from_compose. Enfin, on précise que le mot de passe à utiliser pour utiliser jupyter est bonjour.

services:
  jupyter:
    image: jupyter/minimal-notebook:ubuntu-18.04
    container_name: my_jupyter_from_compose
    networks:
      - my_network_from_compose
    ports:
      - target: 8888
        published: 4444
        protocol: tcp
        mode: host
    environment:
      JUPYTER_TOKEN: "bonjour"
  elasticsearch:
    image: elasticsearch:7.2.0
    container_name: my_es_from_compose
    networks:
      - my_network_from_compose
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      discovery.type: single-node
networks:
  my_network_from_compose:

Mettez à jour le contenu du fichier docker-compose.yml avec ces lignes et relancez ces services

Une fois que les services sont lancés, allez sur le navigateur à l'adresse IP de votre machine virtuelle et le port 4444, vous devrez avoir l'interface jupyter.

Dans un nouveau notebook, exécutez le code suivant:

import requests
import pprint
import json

content = json.loads(requests.get('http://my_es_from_compose:9200').content)

pprint.pprint(content)

Enfin, on peut préciser créer des points de monture en précisant volumes. Par exemple, dans le code suivant, on peut lier le dossier work du container vers le dossier local:

services:
  jupyter:
    image: jupyter/minimal-notebook:ubuntu-18.04
    container_name: my_jupyter_from_compose
    networks:
      - my_network_from_compose
    volumes:
      - .:/home/jovyan/work
    ports:
      - target: 8888
        published: 4444
        protocol: tcp
        mode: host
    environment:
      JUPYTER_TOKEN: "bonjour"
  elasticsearch:
    image: elasticsearch:7.2.0
    container_name: my_es_from_compose
    networks:
      - my_network_from_compose
    ports:
      - "9200:9200"
      - "9300:9300"
    environment:
      discovery.type: single-node
networks:
  my_network_from_compose:

Mettez à jour le fichier, relancez les services et vérifier le contenu du dossier work

Docker Compose permet donc de lancer facilement un ensemble de containers. Nous avons vu ici les principaux arguments de Docker Compose. Nous n'irons pas plus loin car ce n'est pas l'objet de ce cours. Notez que si nous souhaitons lancer des containers sur un système distribué, on utilisera des outils comme Docker Swarm ou Kubernetes.