8.0 Docker Compose

Docker Compose permet de gérer des applications complexes, c’est à dire par exemple des applications qui dialoguent les unes avec les autres. Très simplement nous pouvons utiliser Docker Compose : Grâce à une configuration sous forme de fichier YAML dont le nom par défaut est docker-compose.yml par défaut.

8.1 Structure du fichier docker.compose.yml

Dans ce fichier nous définirons l’ensemble des éléments d’une apllication :

  • Les services.

  • Les volumes.

  • Les Networks qui permettent d’isoler les services.

  • Les secrets (données sensibles nécessaires au fonctionnement de l’application, pris en compte seulement dans un cluster Swarm).

  • Les configs (configuration sensibles nécessaires au fonctionnement de l’application, pris en compte seulement dans un cluster Swarm).

Examinons maintenant une application web branchée à une API configurée dans le fichier docker-compose.yml :

Exemple d’application web
 1  version: '3.9'
 2  volumes:
 3    data:
 4  networks:
 5    frontend:
 6    backend:
 7  services:
 8    web:
 9      images: org/web:2.3
10      networks:
11        - frontend
12      ports:
13        - 80:80
14    api:
15      image: org/api
16      networks:
17        - backend
18        - frontend
19    db:
20      image: mongo
21      volumes:
22        - data:/data/db
23      networks:
24        - backend

Analysons les principales clées de notre fichier :

  • Version correspond à la version du format Compose à mettre en relation avec la version du Docker Daemon de la machine hôte qui va être utilisée pour déployer notre application. Si on utilise la fernière version de compose avec un Daemon plus ancien, il y a certaines options écrites dans le docker-compose.yml qui ne seront pas prises en compte.

  • Volumes permet de définir un volume, que l’on appelle ici, data et qui sera utilisé dans un service par la suite. Par défaut, ce volume utilise le driver local, qui va donc créer un répertoire sur la machine hôte.

  • Networks permet de créer des réseaux qui serviront à isoler des groupes de services.

  • Services contient la définition des services nécessaires au fonctionnement de notre application. Ici, nous avons nos 3 services : WEB, API, DB.

Pour chaque service on spécifie l’image utilisée, les volumes pour la persistance des données.Le service DB est le seul a persister les données et montera le volume data dans le répertoire /data/db du container qui sera lancé.

Pour chaque service, on définit aussi les réseaux attachés avec la clé Networks. Dans notre exemple : le service API```doit pouvoir communiquer avec le service ``WEB et DB. Donc il faut lui donner accès aux deux réseaux attachés à ces services : backend et frontend.

En isolant ainsi les services, on s’assure qu’ils ne puissent pas avoir accès à des services dont ils n’ont pas besoin d’avoir accès directement. Comme le service WEB, qui ne doit pas pouvoir accès au service DB directement. Cela ajoute un niveau de sécurité au cas où l’un des services serait compromis.

La clé ports publie les ports nommés vers l’extérieur pour le service qui a besoin d’être joins, comme le serveur Web et son port 80.

De nombreuses options sont encore disponibles pour la définition d’un service dans le format compose.

Voici une liste des plus utilisées :

  • Image utilisée par le container du service.

  • Nombre de réplicas, c’est à dire le nombre de container identique qui sera lancé pour le service. En augmentant le nombre de container, on pourra traiter les piques de charge par exemple.

  • Les ports publiés à l’extérieur par les services qui ont besoin d’être accessible par l’extérieur.

  • La définition d’un Health check pour vérifier l’état de santé d’un service.

  • Les stratégies de redémarrage de façon à définir que faire si un container a planté par exemple.

  • Contraintes de déploiement (dans un contexte de SWARM uniquement), par exemple pour imposer qu’un container doit tourner sur une machine contenant un disque SSD.

  • Contraintes des mises à jour (dans un contexte de SWARM uniquement).

Un des avantages qu’il y a à déployer une application à travers le fichier docker-compose.yml, c’est qu’elle peut être déployer sur n’importe quel environnement. En utilisant le binaire compose, un développeur peut installer sur une machine son application, avec son environnement de développement complet.

8.2 Le binaire docker-compose

Le binaire docker-compose est utilisé pour gérer une application qui est gérée selon le format docker-compose.yml. Cet outil est indépendant du docker daemon qui est souvent livré en même temps (Docker for Mac, Docker for Windows).

Utilisation de docker-compose
1docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]

Il y a plusieurs éléments qui peuvent être fourni au binaire :

  • Le chemin d’accès aux fichiers dans lequel est défini l’application. ( par défault il s’agit du fichier : docker-compose.yml du répertoire courant)

  • Des options, comme des chemins d’accès à des certificats et clés TLS ou l’adresse de l’hôte à contacter.

  • Une commande pour gérer l’application.

  • Des arguments pour cette commande.

On peut avoir plusieurs fichiers pour spécifier une configuration différente par environnement de développement.

title

Commande

Utilisation

up / down

Création / Suppression d’une application (services, volumes, réseaux)

start / stop

Démarrage / arrête d’une application

build

Build des images des services (si instruction build utilisée)

pull

Téléchargement d’une image

logs

Visualisation des logs de l’application

scale

Modification du nombre de container pour un service

ps

Liste les containers de l’application

8.3 Service discovery

Une application définie par docker-compose est en général constituée de plusieurs services dont certain communique avec d’autres. Nous sommes souvent dans un environnement microservice.

Pour permettre la résolution du service, le dns intégré dans le docker daemon est utilisé.Ainsi nous pouvons résoudre l’IP d’un service à partir de son nom.

Voyons un exemple :

../../../_images/image105.png

Sur la gauche, nous avons un extrait d’une application docker-compose composée de deux services.

Un service est utilisé pour la base de données, db et un pour l”api qui utilise ce service db`.

Nous voyons aussi qu’il y a un volume qui se nomme data et qui est monté dans le service db.

A droite, nous avons une partie du code nodeJs de l”api qui montre comment la connexion à la base de données est réalisée. Il suffit juste de donner le nom du service de base de données dans la chaine de connexion.

C’est quelque chose de très pratique. Toutefois, il faudra ajouter un mécanisme qui permette d’attendre que la db soit disponible ou éventuellement renouveller la tentative de connexion. Docker-compose permet d’indiquer les dépendances entre les services mais il ne permet pas de savoir qu’un service est disponible avant de lancer un service qui en dépend.

8.4 Mise en oeuvre d’une application microservice : Voting App.

../../../_images/image106.png

L’application Voting App est développée et maintenue par Docker. Elle est beaucoup utilisée pour des présentations ou des démos.Nous pouvons la récupérer en local en clonant le répository GitHub .

C’est une application très pratique pour illustrer le concept de microservices.

Elle est composée de :

  • 5 services :

    • 2 bases de données : redis et postgres

    • 3 services développés chacun dans un environnement différent : Python, NodeJs et .NET

Un utilise vote depuis l’interface web, par défaut l’utilisateur doit choisir entre « cat » et « dog ». Le vote est stocké dans la base de données Redis.

Le service Worker, va récupérer le vote depuis Redis et va l’enregister dans la base de données PostGres et les utilisateurs pourront consulter les résultats via l’interface Web fournie par le service Result.

Si nous visitons le dépôt GitHub de l’application, nous constatons qu’il existe plusieurs fichiers docker-compose qui illistrent différentes utilisation de l’application :

Pour la production on aura le fichier docker-stack alors que pour le développement nous aurons plutôt docker-compose. Il est possible de choisir différents langages comme java ou .NET pour le worker. Ainsi que différents OS : Linux ou Windows.

../../../_images/image107.png

Ouvrons le fichier docker-compose-simple.yml

Fichier docker-compose-simple.yml
 1version: "3"
 2
 3services:
 4  vote:
 5    build: ./vote
 6    command: python app.py
 7    volumes:
 8    - ./vote:/app
 9    ports:
10      - "5000:80"
11
12  redis:
13    image: redis:alpine
14    ports: ["6379"]
15
16  worker:
17    build: ./worker
18
19  db:
20    image: postgres:9.4
21    environment:
22      POSTGRES_USER: "postgres"
23      POSTGRES_PASSWORD: "postgres"
24
25  result:
26    build: ./result
27    command: nodemon server.js
28    volumes:
29      - ./result:/app
30    ports:
31      - "5001:80"
32      - "5858:5858"

L’instruction build sert à définir l’emplacement du contexte de construction du service : le dockerfile ainsi que les autres fichiers nécessaire à la construction de l’image.

Pour le service vote, nous voyons que nous avons bien les fichiers de l’application et le Dockerfile dans le dossier vote.

../../../_images/image108.png

Pour les services vote et result, nous définissons dans l’instruction volume le bindmount du code applicatif depuis la machine hôte vers le répertoire /app dans le container. Cela permet de rendre le code source présent sur la machine de développement directement accessible dans le container.

Et une approche qui est souvent utilisée avec Docker-compose en développement est de redéfinir la commande qui est normalement lancée dans le container.

On utilise pour cela le mot clé command comme nous pouvons le voir dans les services vote et result.

Par contre si nous ouvrons le fichier docker-stack.yml, nous avons une définition de l’application prête à être déployer sur un cluster Swarm de production.

Fichier docker-stack.yml
 1  version: "3"
 2  services:
 3
 4    redis:
 5      image: redis:alpine
 6      networks:
 7        - frontend
 8      deploy:
 9        replicas: 1
10        update_config:
11          parallelism: 2
12          delay: 10s
13        restart_policy:
14          condition: on-failure
15    db:
16      image: postgres:9.4
17      environment:
18        POSTGRES_USER: "postgres"
19        POSTGRES_PASSWORD: "postgres"
20      volumes:
21        - db-data:/var/lib/postgresql/data
22      networks:
23        - backend
24      deploy:
25        placement:
26          constraints: [node.role == manager]
27    vote:
28      image: dockersamples/examplevotingapp_vote:before
29      ports:
30        - 5000:80
31      networks:
32        - frontend
33      depends_on:
34        - redis
35      deploy:
36        replicas: 2
37        update_config:
38          parallelism: 2
39        restart_policy:
40          condition: on-failure
41    result:
42      image: dockersamples/examplevotingapp_result:before
43      ports:
44        - 5001:80
45      networks:
46        - backend
47      depends_on:
48        - db
49      deploy:
50        replicas: 1
51        update_config:
52          parallelism: 2
53          delay: 10s
54        restart_policy:
55          condition: on-failure
56
57    worker:
58      image: dockersamples/examplevotingapp_worker
59      networks:
60        - frontend
61        - backend
62      depends_on:
63        - db
64        - redis
65      deploy:
66        mode: replicated
67        replicas: 1
68        labels: [APP=VOTING]
69        restart_policy:
70          condition: on-failure
71          delay: 10s
72          max_attempts: 3
73          window: 120s
74        placement:
75          constraints: [node.role == manager]
76
77    visualizer:
78      image: dockersamples/visualizer:stable
79      ports:
80        - "8080:8080"
81      stop_grace_period: 1m30s
82      volumes:
83        - "/var/run/docker.sock:/var/run/docker.sock"
84      deploy:
85        placement:
86          constraints: [node.role == manager]
87
88  networks:
89    frontend:
90    backend:
91
92  volumes:
93    db-data:

Si l’on regarde dans le service vote par exemple. On peut voir que contrairement au fichier docker-compose-simple, nous n’avons pas l’instruction build mais image. Ce qui est logique puisqu’en production nous déployons les images des services et non pas les codes applicatifs dans les containers.

Nous trouvons également l’instruction deploy qui permet de spécifier un ensemble de propriétés dans le cadre d’un déploiement sur un cluster Swarm comme le nombre de replicas, c’est à dire le nombre de container identique qui seront lancés par le service. Des contraintes de placement, qui indique le type de machine du cluster sur lequel le service sera déployé. On peut également spécifier des conditions de redémarrage, restart_policy ou la façon dont la mise à jour d’un service sera effectué avec update_config. Par exemple,si un service a deux réplicas, on peut vouloir mettre à jour le premier, se donner quelques secondes pour être certain qu’il fonctionne correctement avant de faire la mise à jour du second. C’est le mécanisme de rolling update que l’on verra dans le chapitre sur Swarm.

Dans un contexte de production, on s’assurera également d’isoler des groupes de services par l’intermédiaire de networks. Ici , tout en bas du fichier nous voyons que deux networks sont définis : frontend et backend.

8.5 Voting App Installation sur Play Docker.

Nous allons installer l’application dans un environnement temporaire dans un premier temps : Play With Docker

../../../_images/image109.png

Cela permet de créer une session Docker dans un environment de test en ligne.

Cliquez sur : Add New Instance

../../../_images/image1101.png

Clonez le dépôt git : https://github.com/dockersamples/example-voting-app.git

1git clone https://github.com/dockersamples/example-voting-app.git
../../../_images/image111.png

Naviguez dans le dossier example-voting-app.

1cd example-voting-app

Et lançons maintenant l’application avec Docker-compose. Nous lui indiquons le nom du fichier avec l’option -f. Le paramètre up indique qu’il faut monter l’application et -d qu’il faut rendre la main de la console une fois monté.

1docker-compose -f docker-compose-simple.yml up -d
../../../_images/image112.png

Après le déploiement de l’application, il apparait dans Play with Docker des boutons portant les numéros des ports des applications.

Si l’on regarde le contenu du fichier docker-compose-simple, nous lisons que :

Le service vote publie son port 80 sur le port 5000 de la machine hôte. Et que le service result publie son port 80 sur le port 5001 de la machine hôte.

../../../_images/image113.png

Si l’on clique dessus, nous pourrons avoir accès aux applications ciblées :

Service de vote :

../../../_images/image114.png

Résultats des votes :

../../../_images/image115.png

Essayez en local !

Essayez d’installer cette application en local sur votre propre machine !

8.6 Voting App Installation en local.

Nous allons illustrer l’utilisation de Docker Compose et lancer l’application Voting App. Cette application est très utilisée pour des présentations et démos, c’est un bon exemple d’application micro-services simple.

8.6.1 Vue d’ensemble

L’application Voting App est composée de plusieurs micro-services, ceux utilisés pour la version 2 sont les suivants:

../../../_images/image116.png
  • vote-ui: front-end permettant à un utilisateur de voter entre 2 options

  • vote: back-end réceptionnant les votes

  • result-ui: front-end permettant de visualiser les résultats

  • result: back-end mettant à disposition les résultats

  • redis: database redis dans laquelle sont stockés les votes

  • worker: service qui récupère les votes depuis redis et consolide les résultats dans une database postgres

  • db: database postgres dans laquelle sont stockés les résultats

8.6.2 Récupération des repos

Commandes à taper dans un dossier
1mkdir VotingApp && cd VotingApp
2git clone https://gitlab.com/voting-application/$project

8.6.3 Installation du binaire docker-compose

  • Si vous utilisez Docker for Mac ou Docker for Windows, le binaire docker-compose est déjà installé.

8.6.4 Le format de fichier docker-compose.yml

Plusieurs fichiers, au format Docker Compose, sont disponibles dans config/compose. Ils décrivent l’application pour différents environnements. Le fichier qui sera utilisé par défaut est le fichier docker-compose.yml dont le contenu est le suivant:

docker-compose.yml
 1  services:
 2  vote:
 3    build: ../../vote
 4    # use python rather than gunicorn for local dev
 5    command: python app.py
 6    depends_on:
 7      redis:
 8        condition: service_healthy
 9    ports:
10      - "5002:80"
11    volumes:
12      - ../../vote:/app
13    networks:
14      - front-tier
15      - back-tier
16
17  vote-ui:
18    build: ../../vote-ui
19    depends_on:
20      vote:
21        condition: service_started
22    volumes:
23      - ../../vote-ui:/usr/share/nginx/html
24    ports:
25      - "5000:80"
26    networks:
27      - front-tier
28    restart: unless-stopped
29
30  result:
31    build: ../../result
32    # use nodemon rather than node for local dev
33    command: nodemon server.js
34    depends_on:
35      db:
36        condition: service_healthy
37    volumes:
38      - ../../result:/app
39    ports:
40      - "5858:5858"
41    networks:
42      - front-tier
43      - back-tier
44
45  result-ui:
46    build: ../../result-ui
47    depends_on:
48      result:
49        condition: service_started
50    ports:
51      - "5001:80"
52    networks:
53      - front-tier
54    restart: unless-stopped
55
56  worker:
57    build:
58      context: ../../worker
59      dockerfile: Dockerfile.${LANGUAGE:-dotnet}
60    depends_on:
61      redis:
62        condition: service_healthy
63      db:
64        condition: service_healthy
65    networks:
66      - back-tier
67
68  redis:
69    image: redis:6.2-alpine3.13
70    healthcheck:
71      test: ["CMD", "redis-cli", "ping"]
72      interval: "5s"
73    ports:
74      - 6379:6379
75    networks:
76      - back-tier
77
78  db:
79    image: postgres:13.2-alpine
80    environment:
81      POSTGRES_USER: "postgres"
82      POSTGRES_PASSWORD: "postgres"
83    volumes:
84      - "db-data:/var/lib/postgresql/data"
85    healthcheck:
86      test: ["CMD", "pg_isready", "-U", "postgres"]
87      interval: "5s"
88    ports:
89      - 5432:5432
90    networks:
91      - back-tier
92
93volumes:
94  db-data:
95
96networks:
97  front-tier:
98  back-tier:

Ce fichier est très intéressant car il définit également des volumes et networks en plus des services. Ce n’est cependant pas un fichier destiné à être lancé en production notamment parce qu’il utilise le code local et ne fait pas référence à des images existantes pour les services vote-ui, vote, result-ui, result et worker.

8.6.5 Lancement de l’application

Depuis le répertoire config/compose, lancez l’application à l’aide de la commande suivante (le fichier docker-compose.yml sera utilisé par défaut):

>>> docker-compose up -d

Les étapes réalisées lors du lancement de l’application sont les suivantes:

  • création des networks front-tier et back-tier

  • création du volume db-data

  • construction des images pour les services vote-ui, vote, result-ui, result, worker et récupération des images redis et postgres

  • lancement des containers pour chaque service

8.6.6 Les containers lancés

Avec la commande suivante, listez les containers qui ont été lancés.

>>> docker-compose ps
Liste des containers lancés
1  Name                      Command                  State                            Ports
2----------------------------------------------------------------------------------------------------------------------
3compose_db_1          docker-entrypoint.sh postgres    Up (healthy)   0.0.0.0:5432->5432/tcp,:::5432->5432/tcp
4compose_redis_1       docker-entrypoint.sh redis ...   Up (healthy)   0.0.0.0:6379->6379/tcp,:::6379->6379/tcp
5compose_result-ui_1   /docker-entrypoint.sh ngin ...   Up             0.0.0.0:5001->80/tcp,:::5001->80/tcp
6compose_result_1      docker-entrypoint.sh nodem ...   Up             0.0.0.0:5858->5858/tcp,:::5858->5858/tcp, 80/tcp
7compose_vote-ui_1     /docker-entrypoint.sh ngin ...   Up             0.0.0.0:5000->80/tcp,:::5000->80/tcp
8compose_vote_1        python app.py                    Up             0.0.0.0:5002->80/tcp,:::5002->80/tcp
9compose_worker_1      dotnet Worker.dll                Up

8.6.7 Les volumes créés

Listez les volumes avec la CLI, et vérifiez que le volume défini dans le fichier docker-compose.yml est présent.

>>> docker volume ls

Le nom du volume est prefixé par le nom du répertoire dans lequel l’application a été lancée.

1DRIVER    VOLUME NAME
2local     compose_db-data

Par défaut ce volume correspond à un répertoire créé sur la machine hôte.

8.6.8 Les networks créés

Listez les networks avec la CLI. Les deux networks définis dans le fichier docker-compose.yml sont présents.

>>> docker network ls

De même que pour le volume, leur nom est préfixé par le nom du répertoire.

1NETWORK ID     NAME                 DRIVER    SCOPE
271d0f64882d5   bridge               bridge    local
3409bc6998857   compose_back-tier    bridge    local
4b3858656638b   compose_front-tier   bridge    local
52f00536eb085   host                 host      local
654dee0283ab4   none                 null      local

Note

Comme nous sommes dans le contexte d’un hôte unique (et non dans le contexte d’un cluster Swarm), le driver utilisé pour la création de ces networks est du type bridge. Il permet la communication entre les containers tournant sur une même machine.

8.6.9 Utilisation de l’application

Nous pouvons maintenant accéder à l’application: nous effectuons un choix entre les 2 options depuis l’interface de vote à l’adresse http://localhost:5000. Si vous avez lancé cette application sur un autre hôte que votre machine, vous aurez accès à cette interface à l’adresse http://HOST:5000

../../../_images/image117.png

nous visualisons le résultat depuis l’interface de résultats à l’adresse http://localhost:5001 Si vous avez lancé cette application sur un autre hôte que votre machine, vous aurez accès à cette interface à l’adresse http://HOST:5001

../../../_images/image118.png

8.6.10 Scaling du service worker

Par défaut, un container est lancé pour chaque service. Il est possible, avec l’option --scale, de changer ce comportement et de scaler un service une fois qu’il est lancé. Avec la commande suivante, augmenter le nombre de worker à 2.

$ docker-compose up -d --scale worker=2
compose_db_1 is up-to-date
compose_redis_1 is up-to-date
compose_result_1 is up-to-date
compose_vote_1 is up-to-date
compose_result-ui_1 is up-to-date
compose_vote-ui_1 is up-to-date
Creating compose_worker_2 ... done

Les 2 containers relatifs au service worker sont présents:

$ docker-compose ps
  Name                      Command                  State                            Ports
  ----------------------------------------------------------------------------------------------------------------------
  compose_db_1          docker-entrypoint.sh postgres    Up (healthy)   0.0.0.0:5432->5432/tcp,:::5432->5432/tcp
  compose_redis_1       docker-entrypoint.sh redis ...   Up (healthy)   0.0.0.0:6379->6379/tcp,:::6379->6379/tcp
  compose_result-ui_1   /docker-entrypoint.sh ngin ...   Up             0.0.0.0:5001->80/tcp,:::5001->80/tcp
  compose_result_1      docker-entrypoint.sh nodem ...   Up             0.0.0.0:5858->5858/tcp,:::5858->5858/tcp, 80/tcp
  compose_vote-ui_1     /docker-entrypoint.sh ngin ...   Up             0.0.0.0:5000->80/tcp,:::5000->80/tcp
  compose_vote_1        python app.py                    Up             0.0.0.0:5002->80/tcp,:::5002->80/tcp
  compose_worker_1      dotnet Worker.dll                Up
  compose_worker_2      dotnet Worker.dll                Up

Notes: il n’est pas possible de scaler les services vote-ui et result-ui car ils spécifient tous les 2 un port, plusieurs containers ne peuvent pas utiliser le même port de la machine hôte

$ docker-compose up -d --scale vote-ui=3
...
ERROR: for vote-ui  Cannot start service vote-ui: driver failed programming external connectivity on endpoint compose_vote-ui_2 (6274094570a329e3a4d9bdcdf4d31b7e3a8e3e7e78d3cc362ad56e14341913da): Bind for 0.0.0.0:5000 failed: port is already allocated

8.6.11 Suppression de l’application

Avec la commande suivante, stoppez l’application. Cette commande supprime l’ensemble des éléments créés précédemment à l’exception des volumes (afin de ne pas perdre de données)

$ docker-compose down
Stopping compose_result-ui_1 ... done
Stopping compose_vote-ui_1   ... done
Stopping compose_result_1    ... done
Stopping compose_vote_1      ... done
Stopping compose_worker_1    ... done
Stopping compose_redis_1     ... done
Stopping compose_db_1        ... done
Removing compose_vote-ui_3   ... done
Removing compose_vote-ui_2   ... done
Removing compose_result-ui_1 ... done
Removing compose_vote-ui_1   ... done
Removing compose_result_1    ... done
Removing compose_vote_1      ... done
Removing compose_worker_1    ... done
Removing compose_redis_1     ... done
Removing compose_db_1        ... done
Removing network compose_back-tier
Removing network compose_front-tier

Afin de supprimer également les volumes utilisés, il faut ajouter le flag -v:

>>> docker-compose down -v

Cet exemple illustre l’utilisation de Docker Compose sur l’exemple bien connu de la Voting App dans le cadre d’un hôte unique. Pour déployer cette application sur un environnement de production, il faudrait effectuer des modifications dans le fichier docker-compose, par exemple:

  • utilisation d’images pour les services

  • ajout de service supplémentaires (aggrégateur de logs, terminaison ssl, …)

  • contraintes de déploiement