docker-compose

Applications multi-containers

Besoin

Pour concevoir et déployer des applications fondées sur plusieurs micro-services :

  • BD

  • NoSQL (Mongo etc.)

  • Applications

  • APIs

de nouveaux besoins apparaissent :

  • nécessité de communiquer entre containers

  • possibilité de créer des réseaux ad-hoc mais pas très facile à manipuler

  • besoin de pouvoir décrire dans une syntaxe simplifiée un système de containers - avec des images prédéfinies - ou spécifiées dans de Dockerfiles - communicant naturellement entre elles

Solution : docker-compose

La commande docker-compose repose sur un fichier docker-compose.yml, écrit au format YAML.

Format YAML

  • json en moins verbeux

  • plus lisible, avec identation (2 caractères décalage suffisent)

  • bcp de fichiers de conf utilisent ce format

  • cf exemples

docker ou docker-compose ?

Créons un conteneur nginx avec la commande Docker :

docker run --name web -d -p 8000:80 nginx:alpine

Puis allez visiter http://localhost:8000

On peut vouloir monter un volume dans notre container pour publier une page de notre cru via nginx :

  • créer un dossier de base nommé compose-nginx

  • créer dedans dossier app contenant un fichier index.html de base du style :

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Nginx Docker</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
<section class="section">
    <div class="container">
    <h1 class="Nginx via Docker">
        Hello World
    </h1>
    <p class="subtitle">
       Nginx  à l'intérieur d'un container <strong>Docker</strong>!
    </p>
    </div>
</section>
</body>
</html>
  • Puis relançons la commande en montant app au bon endroit :

docker run --name web -itd -p 8000:80 -v $(pwd)/app:/usr/share/nginx/html nginx:alpine

Nginx avec docker-compose

Faisons plus simple avec une description en yaml :

version: '3.9'
services:
    web:
        image: nginx:alpine
        ports:
            - "8000:80"
        volumes:
            - ./app:/usr/share/nginx/html

Explications

  • version entre 2 et 3.9. Mettez 3 ou 3.3 si version de docker pas très à jour …

  • top level : services

  • ici un seul service : web, assuré par nginx

  • le volume local app sera visible dans le container, à l’emplacement /usr/share/nginx/html

  • le port 80 de l’intérieur du container sera visible dans l’hôte sur http://localhost:8000

  • Lancement : docker-compose up -d en mode detached

Questions

  • Peut-on changer en direct le contenu du fichier index.html du dossier app pendant que le conteneur tourne ?

  • comment lister ce qui tourne ?

  • Quels volumes sont montés ?

  • Comment tout arrêter ?

Services

On peut évidemment placer de mutiples services dans un docker-compose.

  • les différents services sont décrits avec le même niveau d’indenation.

  • les services peuvent se décrire avec une image préfabriquée (Dockerhub) * avec d’éventuels fichiers de configuration * avec d’éventuels paramètres

  • les services peuvent également faire référence à des images fabriquées avec des Dockerfile spécifiques

principales commandes docker-compose

Commandes

Utilisation

docker-compose build

build

docker-compose up

lancer l’app

docker-compose up -d

lancer en arrière plan

docker-compose ps

lister les containers de l’app

docker-compose logs nginx

visualiser les logs du container nginx

docker-compose pause

faire une pause en gardant les containers en l’état

docker-compose unpause

arrêter la pause

docker-compose stop

arrêter l’application en gardant les données associées

docker-compose down

arrêter l’application en enlevant les containers, réseaux et volumes associés

docker-compose down --rmi all

grand ménage ! (Attention données, etc.)

Testez-les !

Postgresql avec adminer

Une première application muti-container où l’on se contente d’images prédéfinies.

Le docker-compose

# Use postgres/example user/password credentials
version: '3.1'
services:
    db:
        image: postgres
        restart: always
        environment:
          POSTGRES_PASSWORD: example

    adminer:
        image: adminer
        restart: always
        ports:
          - 8080:8080
  • Placez le dans un répertoire séparé et testez !

  • Quels sont les services ?

  • A quoi correspondent les directives restart: always` et environment` ?

  • Adminer tourne-t-il sur son port par défaut ?

Exemple multi-containers avec Dockerfile : Flask et Redis

  • Exemple classique

  • Redis est un sytème clef/valeur efficace dans le Cloud

  • Flask est un micro-framework Python pour développer simplement des app Web

  • Tout le code de Flask est dans un seul fichier run.py

  • Créer un répertoire compose-flask-redis contenant le fichier suivant

Le fichier run.py

from flask import Flask
from redis import Redis

app = Flask(__name__)
redis = Redis(host='redis', port=6379)

@app.route('/')
def hello():
    count = redis.incr('hits')
    return 'Hello for the {} time !\n'.format(count)

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug = True)

Conteneuriser ce service

  • installer les dépendances

  • lancer l’application automatiquement

Les dépendances sont gérées en Python à l’aide d’un fichier requirements.txt qui peut être utilisé pour créer un virtualenv ou un conteneur.

A minima :

flask
redis

(on peut préciser des versions aussi !)

On installe les requirements avec la commande : pip install -r requirements.txt

  • Ecrire le Dockerfile pour le service Flask en partant de l’image python:3.11-slim

  • Ajouter le contenu du dossier le contenu du dossier courant au dossier /app du conteneur : ADD . /app

  • puis choisir /app comme répertoire de travail

  • installer les dépendances

  • exposer le port 5000

  • lancer run.py

  • testez !

le docker compose avec le service Redis

Ajouter à présent un docker-compose.yml dans votre dossier dont voici les éléments :

  • Redis étant une image standard on peut directement l’invoquer dans le docker-compose contenant deux services :

redis:
  image: "redis"
  • le service Flask étant décrit par

web:
    build: .
    ports:
    - "4000:5000"
  • Ecrivez le docker-compose correspondant

  • puis build et lancement

  • docker-compose build

  • docker-compose up -d

Visitez : http://127.0.0.1:4000

Observation de l’app avec les outils Docker

  • docker-compose ps

  • docker logs ...

  • Quelles sont les tailles des images utilisées ?

  • Lister les réseaux ? Les volumes ?

  • Comment lancer un terminal interactif sur le container Flask ?

  • Comment arrêter tout ?

  • Faire le grand ménage ?

Améliorations du Dockerfile

  • On peut définir des variables d’environnement dans FLASK : FLASK_DEBUG à la valeur True si on souhaite être en mode DEBUG et FLASK_APP avec le nom de l’app à lancer.

  • puis lancer l’app avec la directive : flask run

Effectuez les petites modifs correspondantes. Tester !

Réduction des images

  • essayez les images python:3.11-alpine et python:3.11

  • comparez les tailles et les temps de build et de lancement

Outils complémentaires pour Docker

lazydocker

../_images/lazydocker.png

lazydocker est un outil en ligne de commande qui fournit une interface utilisateur pour les conteneurs Docker et Docker Compose. Il fournit une vue d’ensemble des conteneurs en cours d’exécution, des images, des volumes et des réseaux, ainsi que des informations détaillées sur chaque conteneur. L’interface utilisateur est basée sur la bibliothèque de terminal TUI Go mais n’est pas le plus abouti au niveau de l’interface. Il est néanmoins simple d’utilisation et permet de gérer les conteneurs Docker et Docker Compose. Il s’installe via la commande suivante sous Linux :

$ apt install lazydocker

Le site du projet est: https://github.com/jesseduffield/lazydocker

portainer

../_images/portainer.png

Portainer est un outil web qui permet de gérer les conteneurs Docker et Docker Compose. Il permet de gérer les conteneurs, les images, les volumes, les réseaux, les stacks etc. Il est disponible sous forme d’image Docker et peut donc s’installer via Docker ! On peut par exemple le configurer et le lancer à partir du docker-compose suivant (linux ou mac) :

version: '3'

services:
portainer:
    image: portainer/portainer-ce:latest
    container_name: portainer
    restart: unless-stopped
    security_opt:
        - no-new-privileges:true
    volumes:
        - /etc/localtime:/etc/localtime:ro
        - /var/run/docker.sock:/var/run/docker.sock:ro
        - ./portainer-data:/data
    ports:
        - 9000:9000
    # environment:
    #    TZ: Europe/Paris
    #    PORTAINER_DATA: /data

(la partie environment est commentée car elle n’est pas nécessaire sous Unix) On lance la commande suivante pour lancer le service portainer :

$ docker-compose up -d

L’interface web est accessible via l’adresse suivante : http://localhost:9000. Lors du premier lancement, un assistant de configuration est lancé pour créer un compte administrateur, puis choisir l’option « local » pour la connexion à Docker. Il faut nommer le container et puis cliquer sur « connect ». Vous pouvez ensuite visualiser les conteneurs Docker et Docker Compose lancés sur la machine ainsi que leurs environments, volumes, ports, etc. L’interface est très complète et permet de gérer les conteneurs ou des clusters Kubernetes. La documentation est disponible ici : https://docs.portainer.io

yamllint

Vérifiez la syntaxe de votre fichier YAML avec la commande suivante :

$ yamllint docker-compose.yml

La doc est ici : https://yamllint.readthedocs.io/en/stable/

autres outils

Scaling docker-compose avec haproxy

  1. Mettre en place la structure de projet suivante :

.
├── app
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── run.py
│   └── venv
├── docker-compose.yml
└── haproxy
   └── haproxy.cfg
  1. Dans le dossier app :

Un fichier Dockerfile :

FROM python:3.11-alpine
WORKDIR /code
COPY run.py .
COPY requirements.txt .
RUN pip install virtualenv
RUN virtualenv venv
RUN source venv/bin/activate
RUN pip install -r requirements.txt
ENV FLASK_APP=run.py
ENV FLASK_DEBUG=True
EXPOSE 5000
ENTRYPOINT ["python", "run.py"]

Un fichier requirements.txt :

Flask
redis

Le fichier run.py :

from flask import Flask
import redis
import os
import socket

app = Flask(__name__)
redis = redis.Redis(host='redis', port=6379)

@app.route('/')
def hello():
    redis.incr('hits')
    return 'Hello World! I have been seen {} times.\n'.format(redis.get('hits'))

@app.route('/hostname')
def hostname():
    return socket.gethostname()

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug = True)
  1. Le fichier haproxy.cfg

Voici l’intégralité du fichier haproxy.cfg à utiliser :

# haproxy config file
# on utilise le dns embarqué de Docker
# la directive server-template web- 4 web:5000 signifie qu'on peut avoir jusqu'à
# 4 containers basés sur le template du service web

defaults
   mode http
   balance roundrobin
   timeout client 60s
   timeout connect 60s
   timeout server 60s

# cf https://www.haproxy.com/documentation/hapee/latest/configuration/config-sections/resolvers/#
resolvers docker
   nameserver dns1 127.0.0.11:53
   hold valid    10s
   hold other    30s
   hold refused  30s
   hold nx       30s
   hold timeout  30s
   hold obsolete 30s
   # How many times to retry a query
   resolve_retries 3
   # How long to wait between retries when no valid response has been received
   timeout retry 1s
   # How long to wait for a successful resolution
   timeout resolve 1s

frontend http
   bind *:80
   mode http
   use_backend all

backend all
   mode http
   server-template web- 4 web:5000 check resolvers docker init-addr libc,none

listen stats
   bind *:80
   mode http
   stats enable
   stats uri /stats
   stats refresh 10s
  1. Mettez en place le docker-compose.yml correspondant

Le service « web » :

web:
   build: .
   ports:
      - 5000
   deploy:
      mode: replicated
      replicas: 4

Avec le Dockerfile dans le dossier app :

Le service redis ne change pas et le service lb (load balancing) est assez simple :

lb:
   image: haproxy
   ports:
      - "4000:80"
   volumes:
      - ./haproxy:/usr/local/etc/haproxy

Quel lien de dépendance faut-il ajouter à ce service ?

Au total le docker-compose.yml devrait ressembler à ceci :

version: '3.9'
services:
   web:
      build:
         context: ./app
         dockerfile: Dockerfile
      deploy:
         mode: replicated
         replicas: 4
      depends_on:
         - redis
   redis:
      image: "redis:7-alpine"
   lb:
      image: haproxy
      depends_on:
         - web
      ports:
         - "4000:80"
      volumes:
         - ./haproxy:/usr/local/etc/haproxy
  1. Puis lancement avec scaling :

docker-compose up -d --scale web=4
  • Testez en consultant: http://127.0.0.1:4000

  • Que se passe-t-il si on enlève au docker-compose la directive : port : 5000 pour le service web ?

Pour des configurations avec du routage avancé et de l’équilibrage de charge (load balancing), voir les derniers slides du cours et la section sur l’utilisation de Traefik plus loin : traefik.

Aller plus loin avec docker-compose

Application Apache PHP Mariadb

Architecture du projet

.
├── Dockerfile
├── app
│   └── public
│       └── index.php
├── db
├── docker-compose.yml
├── log
└── vhosts
   └── 000-default.conf

Le docker-compose.yml

Service Web :

web_app:
  build: .
  container_name: web-app
  depends_on:
     - db_app
  ports:
     - "8000:80"
  volumes:
     - ./log:/var/log/apache2
     - ./app:/var/www/html
     - ./vhosts:/etc/apache2/sites-enabled

A quoi vont servir les volumes montés ?

Service db :

db_app:
   image: mariadb:10.5.8
   container_name: data_app
   restart: always
   environment:
         - MYSQL_ROOT_PASSWORD=xxx
         - MYSQL_DATABASE=db1
   volumes:
         - ./db:/var/lib/mysql
   ports:
         - "3306:3306"
  • A quoi sert la directive restart: always ?

  • Quel est le volume monté ? Pourquoi ?

  • Quel est le port naturel de mariadb ?

  • Est-ce qu c’est vraiment le bon endroit pour définir le password de root ?

  • Comment faire autrement ?

Le Dockerfile

# On utilise l'image php8.1-apache
FROM php:8.1-apache
# A quoi sert la ligne suivante ?
RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
# On installe quelques dépendances pour composer et les extensions PHP
RUN apt-get update \
   && apt-get install -y --no-install-recommends locales apt-utils git libicu-dev g++ libpng-dev libxml2-dev libzip-dev libonig-dev libxslt-dev;
# On met en place les locales
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
   echo "fr_FR.UTF-8 UTF-8" >> /etc/locale.gen && locale-gen
# On télécharge composer pour gérer les projets PHP
# pourquoi le déplace-t-on ?
RUN curl -sSk https://getcomposer.org/installer | php -- --disable-tls && \
   mv composer.phar /usr/local/bin/composer
# on installe et configure les extensions PHP souhaitées
RUN docker-php-ext-configure intl
RUN docker-php-ext-install pdo pdo_mysql gd opcache intl zip calendar dom mbstring zip gd xsl
RUN pecl install apcu && docker-php-ext-enable apcu
# On se place dans le répertoire de publication d'Apache
WORKDIR /var/www/
VOLUME ["/var/www/html","/var/log/apache2","/etc/apache2/sites-enabled"]

Enfin le vhost d’Apache :

<VirtualHost *:80>
 ServerName localhost

 DocumentRoot /var/www/html/public
 DirectoryIndex /index.php

 <Directory /var/www/html/public>
     AllowOverride None
     Order Allow,Deny
     Allow from All

     FallbackResource /index.php
 </Directory>
 <Directory /var/www/html/public/bundles>
     FallbackResource disabled
 </Directory>
 ErrorLog /var/log/apache2/project_error.log
 CustomLog /var/log/apache2/project_access.log combined
</VirtualHost>

et le fichier index.php

<?php phpinfo(); >

Testez !

docker-compose avancé

Architecture du projet

├── Readme.md
├── docker
│   ├── db
│   │   └── mariadb
│   │       └── my.cnf
│   └── server
│       ├── Dockerfile
│       ├── apache
│       │   └── sites-enabled
│       │       └── site.conf
│       └── php
│           └── php.ini
├── docker-compose.yml
└── symfony

Fichier .env

APP_NAME=symfony_projet
APP_PORT=8100
APP_DB_ADMIN_PORT=8200
DB_PORT=33016

MYSQL_ROOT_PASS=supersecret
MYSQL_USER=app_user
MYSQL_PASS=secretpasswd
MYSQL_DB=symfony_projet

Fichier my.cnf

[mysqld]
collation-server = utf8mb4_unicode_ci
character-set-server = utf8mb4

Fichier site.conf

<VirtualHost *:80>
   DocumentRoot /var/www/html/public
   <Directory /var/www/html/public>
         AllowOverride None
         Order Allow,Deny
         Allow from All

         <IfModule mod_rewrite.c>
            Options -MultiViews
            RewriteEngine On
            RewriteCond %{REQUEST_FILENAME} !-f
            RewriteRule ^(.*)$ index.php [QSA,L]
         </IfModule>
   </Directory>
</VirtualHost>

fichier Dockerfile dans docker/server

FROM php:8.1-apache

RUN a2enmod rewrite

RUN apt-get update && apt-get install -y git unzip zip

WORKDIR /var/www/html/

COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/

RUN install-php-extensions gd pdo_mysql bcmath zip intl opcache

COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer

# met à jour les informations pour git
RUN git config --global user.email "roza@univ-orleans.fr" && git config --global user.name "Gérard Rozsa"

RUN curl -sS https://get.symfony.com/cli/installer | bash \
&& mv /root/.symfony5/bin/symfony /usr/local/bin/symfony
## && symfony new symfony_project --version=stable --dir=/var/www/symfony

Fichier docker-compose.yml

version: '3.9'

networks:
symfony_projet_net:

services:
   server:
      build:
         context: .
         dockerfile: ./docker/server/Dockerfile
      container_name: '${APP_NAME}-server'
      ports:
         - '${APP_PORT}:80'
      working_dir: /var/www/html
      environment:
         - 'DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASS}@db_server:3306/${MYSQL_DB}?serverVersion=10.9'
      volumes:
         - ./symfony:/var/www/html
         - ./docker/server/apache/sites-enabled:/etc/apache2/sites-enabled
         - ./docker/server/php/php.ini:/usr/local/etc/php/conf.d/extra-php-config.ini
      depends_on:
         db_server:
         condition: service_healthy
      networks:
         - symfony_projet_net

   db_server:
      image: mariadb:10.9.3
      container_name: '${APP_NAME}-db'
      restart: always
      ports:
         - '${DB_PORT}:3306'
      environment:
         MYSQL_ROOT_PASSWORD: '${MYSQL_ROOT_PASS}'
         MYSQL_USER: '${MYSQL_USER}'
         MYSQL_PASSWORD: '${MYSQL_PASS}'
         MYSQL_DATABASE: '${MYSQL_DB}'
      volumes:
         - db_data:/var/lib/mysql
         - ./docker/db/mariadb/my.cnf:/etc/mysql/conf.d/my.cnf
      healthcheck:
         test: mysqladmin ping -h 127.0.0.1 -u root --password=$$MYSQL_ROOT_PASSWORD
         interval: 5s
         retries: 5
      networks:
         - symfony_projet_net

   db_admin:
      image: phpmyadmin/phpmyadmin:5
      container_name: '${APP_NAME}-db-admin'
      ports:
         - '${APP_DB_ADMIN_PORT}:80'
      environment:
         PMA_HOST: db_server
      depends_on:
         db_server:
         condition: service_healthy
      volumes:
         - db_admin_data:/var/www/html
      networks:
         - symfony_projet_net

volumes:
   db_data:
   db_admin_data:

Observations/Questions

1. Observez l’usage des variables d’environnement dans .env. Ces variables peuvent ensuite être réutilisées dans les fichiers Docker comme dans docker-compose.yml : DATABASE_URL=mysql://${MYSQL_USER}:${MYSQL_PASS}@db_server:3306/${MYSQL_DB}?serverVersion=10.5`

  1. Observez les services. Combien y en a-t-il ?

  2. Observez le Dockerfile. Quelle est son utilité ? Ses différentes étapes ? Expliquez le fonctionnement des commandes COPY utilisées.

  3. Quels sont les volumes montés et leurs rôles ?

  4. Tester le service phpmyadmin.

  • Sur quel port tourne-t-il ?

  • Pourrait-on mettre ce numéro de port en paramètre ?

  • Quels sont les identifiants pour se connecter ?

  1. Pour le service correspondant à Apache/Symfony :

  • ouvrez un terminal sur le conteneur concerné : docker exec -it symfony_projet-server bash

  • placez-vous dans /var/www/html

  • Lancez la commande : composer create-project symfony/website-skeleton .

  • Qu’observez-vous dans votre dossier de travail ?

  • Testez si Symfony est bien accessible

Installer Wordpress avec docker-compose

Il suffit d’un docker-compose avec 3 services :

  • un service mariadb

  • un service wordpress

  • un service phpmyadmin

Le fichier docker-compose.yml

version: '3.3'

services:
    db:
        image: mariadb:10.9.3
        volumes:
            - db_data:/var/lib/mysql
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: password
            MYSQL_DATABASE: wordpress
            MYSQL_USER: wordpress
            MYSQL_PASSWORD: wordpress
    phpmyadmin:
        depends_on:
            - db
        image: phpmyadmin/phpmyadmin
        restart: always
        ports:
            - '8080:80'
        environment:
            PMA_HOST: db
            MYSQL_ROOT_PASSWORD: password
    wordpress:
        depends_on:
            - db
        image: wordpress:latest
        ports:
            - '8000:80'
        restart: always
        volumes: ['./wp:/var/www/html']
        environment:
            WORDPRESS_DB_HOST: db:3306
            WORDPRESS_DB_USER: wordpress
            WORDPRESS_DB_PASSWORD: wordpress
volumes:
    db_data:

On lance avec la commande :

docker-compose up -d

On peut vérifier que tout est bien lancé avec la commande :

docker-compose ps

Puis on visite l’adresse http://localhost:8000 pour voir le site wordpress qui présente un petit assistant d’installation.

Déploiement sur Gitpod.io

Gitpod.io permet de déployer une app hébergée sur github ou gitlab dans un environnement de développement en ligne containeurisé.

Exemples :

Pour déployer une app sur gitpod, il suffit de créer un fichier .gitpod.yml à la racine du projet qui décrit les étapes de créations de l’environnement de développement, un peu à la manière d’un Dockerfile.

Intégration continue sur Gitlab

On peut définir un ou plusieurs pipelines dans le fichier .gitlab-ci.yml qui décrit les étapes de création de l’environnement d’intégration continue, à la manière d’un docker-compose.

Voir :

Tests et Gitlab CI (Intégration continue)

pour en savoir plus !