Introduction à Docker Compose

1. Vue d'ensemble

Lors de l'utilisation intensive de Docker, la gestion de plusieurs conteneurs différents devient rapidement fastidieuse.

Docker Compose est un outil qui nous aide à surmonter ce problème et à gérer facilement plusieurs conteneurs à la fois.

Dans ce tutoriel, nous examinerons ses principales fonctionnalités et ses puissants mécanismes.

2. La configuration YAML expliquée

En bref, Docker Compose fonctionne en appliquant de nombreuses règles déclarées dans un seul fichier de configuration docker-compose.yml .

Ces règles YAML, à la fois lisibles par l'homme et optimisées par machine, nous fournissent un moyen efficace de prendre un instantané de l'ensemble du projet à partir de dix mille pieds en quelques lignes.

Presque chaque règle remplace une commande Docker spécifique, de sorte qu'à la fin, nous devons simplement exécuter:

docker-compose up

Nous pouvons obtenir des dizaines de configurations appliquées par Compose sous le capot. Cela nous évitera d'avoir à les écrire avec Bash ou autre chose.

Dans ce fichier, nous devons spécifier la version du format de fichier Compose, au moins un service et éventuellement des volumes et des réseaux :

version: "3.7" services: ... volumes: ... networks: ... 

Voyons ce que sont réellement ces éléments.

2.1. Prestations de service

Tout d'abord, les services font référence à la configuration des conteneurs .

Par exemple, prenons une application Web dockerisée composée d'un frontal, d'un back-end et d'une base de données: nous diviserions probablement ces composants en trois images et les définirions comme trois services différents dans la configuration:

services: frontend: image: my-vue-app ... backend: image: my-springboot-app ... db: image: postgres ... 

Il existe plusieurs paramètres que nous pouvons appliquer aux services, et nous les explorerons en profondeur plus tard.

2.2. Volumes et réseaux

Les volumes , en revanche, sont des zones physiques d'espace disque partagées entre l'hôte et un conteneur, voire entre des conteneurs. En d'autres termes, un volume est un répertoire partagé dans l'hôte , visible depuis certains ou tous les conteneurs.

De même, les réseaux définissent les règles de communication entre les conteneurs et entre un conteneur et l'hôte . Les zones de réseau communes rendront les services des conteneurs détectables les uns par les autres, tandis que les zones privées les sépareront dans des sandbox virtuels.

Encore une fois, nous en apprendrons plus à leur sujet dans la section suivante.

3. Dissection d'un service

Commençons maintenant à inspecter les principaux paramètres d'un service.

3.1. Tirer une image

Parfois, l'image dont nous avons besoin pour notre service a déjà été publiée (par nous ou par d'autres) dans Docker Hub ou dans un autre registre Docker.

Si tel est le cas, nous y faisons référence avec l' attribut image , en spécifiant le nom de l'image et la balise:

services: my-service: image: ubuntu:latest ... 

3.2. Construire une image

Au lieu de cela, nous pourrions avoir besoin de créer une image à partir du code source en lisant son Dockerfile .

Cette fois, nous utiliserons le mot-clé build , en passant le chemin d'accès au Dockerfile comme valeur:

services: my-custom-app: build: /path/to/dockerfile/ ... 

Nous pouvons également utiliser une URL au lieu d'un chemin:

services: my-custom-app: build: //github.com/my-company/my-project.git ... 

De plus, nous pouvons spécifier un nom d' image en conjonction avec l' attribut build , qui nommera l'image une fois créée, la rendant disponible pour être utilisée par d'autres services:

services: my-custom-app: build: //github.com/my-company/my-project.git image: my-project-image ... 

3.3. Configuration du réseau

Les conteneurs Docker communiquent entre eux dans des réseaux créés, implicitement ou par configuration, par Docker Compose . Un service peut communiquer avec un autre service sur le même réseau en le référençant simplement par nom de conteneur et port (par exemple network-example-service: 80 ), à condition que nous ayons rendu le port accessible via le mot-clé expose :

services: network-example-service: image: karthequian/helloworld:latest expose: - "80" 

Dans ce cas, en passant, cela fonctionnerait également sans l'exposer, car la directive expose est déjà dans l'image Dockerfile.

Pour atteindre un conteneur depuis l'hôte , les ports doivent être exposés de manière déclarative via le mot clé ports , ce qui nous permet également de choisir si l'exposition du port différemment dans l'hôte:

services: network-example-service: image: karthequian/helloworld:latest ports: - "80:80" ... my-custom-app: image: myapp:latest ports: - "8080:3000" ... my-custom-app-replica: image: myapp:latest ports: - "8081:3000" ... 

Le port 80 sera désormais visible depuis l'hôte, tandis que le port 3000 des deux autres conteneurs sera disponible sur les ports 8080 et 8081 de l'hôte. Ce mécanisme puissant nous permet d'exécuter différents conteneurs exposant les mêmes ports sans collisions .

Enfin, nous pouvons définir des réseaux virtuels supplémentaires pour séparer nos conteneurs:

services: network-example-service: image: karthequian/helloworld:latest networks: - my-shared-network ... another-service-in-the-same-network: image: alpine:latest networks: - my-shared-network ... another-service-in-its-own-network: image: alpine:latest networks: - my-private-network ... networks: my-shared-network: {} my-private-network: {} 

Dans ce dernier exemple, nous pouvons voir qu'un autre-service-dans-le-même-réseau sera capable de cingler et d'atteindre le port 80 du service-exemple-de-réseau , tandis qu'un autre-service-dans-son-propre-réseau a gagné 't.

3.4. Configuration des volumes

Il existe trois types de volumes: les volumes anonymes , nommés et hôtes .

Docker gère à la fois les volumes anonymes et nommés , en les montant automatiquement dans des répertoires auto-générés dans l'hôte. Alors que les volumes anonymes étaient utiles avec les anciennes versions de Docker (avant 1.9), les volumes nommés sont la solution suggérée de nos jours. Les volumes d'hôte nous permettent également de spécifier un dossier existant dans l'hôte.

Nous pouvons configurer des volumes hôtes au niveau du service et des volumes nommés au niveau externe de la configuration, afin de rendre ces derniers visibles aux autres conteneurs et pas seulement à celui auquel ils appartiennent:

services: volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/my-volumes/named-global-volume - /tmp:/my-volumes/host-volume - /home:/my-volumes/readonly-host-volume:ro ... another-volumes-example-service: image: alpine:latest volumes: - my-named-global-volume:/another-path/the-same-named-global-volume ... volumes: my-named-global-volume: 

Ici, les deux conteneurs auront un accès en lecture / écriture au dossier partagé my-named-global-volume , quels que soient les différents chemins auxquels ils l'ont mappé. Les deux volumes hôtes, à la place, ne seront disponibles que pour volumes-example-service .

Le dossier / tmp du système de fichiers de l'hôte est mappé au dossier / my-volumes / host-volume du conteneur.

Cette partie du système de fichiers est accessible en écriture, ce qui signifie que le conteneur peut non seulement lire mais aussi écrire (et supprimer) des fichiers sur la machine hôte.

We can mount a volume in read-only mode by appending :ro to the rule, like for the /home folder (we don't want a Docker container erasing our users by mistake).

3.5. Declaring the Dependencies

Often, we need to create a dependency chain between our services, so that some services get loaded before (and unloaded after) other ones. We can achieve this result through the depends_on keyword:

services: kafka: image: wurstmeister/kafka:2.11-0.11.0.3 depends_on: - zookeeper ... zookeeper: image: wurstmeister/zookeeper ... 

We should be aware, however, that Compose will not wait for the zookeeper service to finish loading before starting the kafka service: it will simply wait for it to start. If we need a service to be fully loaded before starting another service, we need to get deeper control of startup and shutdown order in Compose.

4. Managing Environment Variables

Working with environment variables is easy in Compose. We can define static environment variables, and also define dynamic variables with the ${} notation:

services: database: image: "postgres:${POSTGRES_VERSION}" environment: DB: mydb USER: "${USER}" 

There are different methods to provide those values to Compose.

For example, one is setting them in a .env file in the same directory, structured like a .properties file, key=value:

POSTGRES_VERSION=alpine USER=foo

Otherwise, we can set them in the OS before calling the command:

export POSTGRES_VERSION=alpine export USER=foo docker-compose up 

Finally, we might find handy using a simple one-liner in the shell:

POSTGRES_VERSION=alpine USER=foo docker-compose up 

We can mix the approaches, but let's keep in mind that Compose uses the following priority order, overwriting the less important with the higher ones:

  1. Compose file
  2. Shell environment variables
  3. Environment file
  4. Dockerfile
  5. Variable not defined

5. Scaling & Replicas

In older Compose versions, we were allowed to scale the instances of a container through the docker-compose scale command. Newer versions deprecated it and replaced it with the scale option.

On the other side, we can exploit Docker Swarm – a cluster of Docker Engines – and autoscale our containers declaratively through the replicas attribute of the deploy section:

services: worker: image: dockersamples/examplevotingapp_worker networks: - frontend - backend deploy: mode: replicated replicas: 6 resources: limits: cpus: '0.50' memory: 50M reservations: cpus: '0.25' memory: 20M ... 

Under deploy, we can also specify many other options, like the resources thresholds. Compose, however, considers the whole deploy section only when deploying to Swarm, and ignores it otherwise.

6. A Real-World Example: Spring Cloud Data Flow

While small experiments help us understanding the single gears, seeing the real-world code in action will definitely unveil the big picture.

Spring Cloud Data Flow is a complex project, but simple enough to be understandable. Let's download its YAML file and run:

DATAFLOW_VERSION=2.1.0.RELEASE SKIPPER_VERSION=2.0.2.RELEASE docker-compose up 

Compose will download, configure, and start every component, and then intersect the container's logs into a single flow in the current terminal.

It'll also apply unique colors to each one of them for a great user experience:

We might get the following error running a brand new Docker Compose installation:

lookup registry-1.docker.io: no such host

While there are different solutions to this common pitfall, using 8.8.8.8 as DNS is probably the simplest.

7. Lifecycle Management

Let's finally take a closer look at the syntax of Docker Compose:

docker-compose [-f ...] [options] [COMMAND] [ARGS...] 

While there are many options and commands available, we need at least to know the ones to activate and deactivate the whole system correctly.

7.1. Startup

We've seen that we can create and start the containers, the networks, and the volumes defined in the configuration with up:

docker-compose up

After the first time, however, we can simply use start to start the services:

docker-compose start

In case our file has a different name than the default one (docker-compose.yml), we can exploit the -f and file flags to specify an alternate file name:

docker-compose -f custom-compose-file.yml start

Compose can also run in the background as a daemon when launched with the -d option:

docker-compose up -d

7.2. Shutdown

Pour arrêter en toute sécurité les services actifs, nous pouvons utiliser stop , qui préservera les conteneurs, volumes et réseaux, ainsi que toutes les modifications qui y sont apportées:

docker-compose stop

Pour réinitialiser l'état de notre projet, au contraire, nous courons tout simplement vers le bas , qui va détruire tout avec seulement l'exception des volumes extérieurs :

docker-compose down

8. Conclusion

Dans ce didacticiel, nous avons découvert Docker Compose et son fonctionnement.

Comme d'habitude, nous pouvons trouver le fichier source docker-compose.yml sur GitHub, ainsi qu'une batterie de tests utiles immédiatement disponibles dans l'image suivante: