Récemment, je me suis lancé dans la création d'un environnement de développement basé sur Docker. L'objectif était de gagner du temps notamment pour faire du développement Web, en ayant déjà un environnement prêt à l'emploi pour développer une application.
J'aurais très bien pu utiliser une machine virtuelle réutilisable en créant un template sur un hyperviseur. Cependant, cette solution aurait été moins pratique et beaucoup plus lourde. J'ai donc choisi Docker pour les raisons suivantes :
- Légèreté
- Approche par microservices
- Multiplateforme
- Facile à mettre en oeuvre
Docker
Avant de vous présenter mon environnement de développement basé sur Docker, faisons un petit rappel de ce qu'est Docker.
Docker est un système open source de gestion de conteneur. Il permet de packager des applications complète avec toutes leurs dépendances dans des conteneurs. Les conteneurs sont un peu comme des machines virtuelles, mais ils sont plus flexibles et plus légers. Les principales différences entre un conteneur et une machine virtuelle sont leur architecture et leur approche.
Une machine virtuelle peut être vue comme un ordinateur entier. Elle dispose d'un OS différent de celui de la machine qui l'héberge. La machine virtuelle peut aussi avoir du matériel (virtuel) différent de celui de la machine source.
Le conteneur quant à lui a plutôt une approche de microservices, un conteneur contient un service (par exemple apache). Il ne contient pas un OS complet. Les conteneurs s'exécutent de manière isolée, quel que soit l'environnement sous-jacent (OS et matériel). De plus, il est important de voir un conteneur comme un environnement avec des données non-pérenne. Là où une machine virtuelle contient les données et les conserves même après un redémarrage, un conteneur Docker ne conserve pas les données qui vont être écrites lorsqu'il est en fonctionnement. Cependant, pour quand même conserver nos données, on utilise la notion de volume, mais on en reparlera plus tard.
Pour que la différence entre un conteneur et une machine virtuelle soit plus claire pour vous voici un schéma comparatif de leur architecture.

Pour bien comprendre le fonctionnement de Docker, il est important de bien comprendre les fondamentaux de ce dernier.
Docker Engine
Docker Engine est le coeur du système Docker, c'est lui qui orchestre tout. Le Docker Engine est une architecture client-serveur, cette architecture est composé de trois éléments qui sont :
- Docker Daemon aussi appelé dockerd, est le service Docker qui est exécuté en permanence sur la machine hôte.
- Docker REST API est l'API qui permet de communiquer avec Docker Daemon, il permet au développeur d'automatiser la création et gestion de conteneur.
- Docker CLI est l'interface client qui permet d'exécuter les différentes commandes Docker (ex : docker build, docker ps, docker run, etc...).
Image Docker
Une image Docker est un package qui contient notre application et tout ce dont elle a besoin pour fonctionner (librairies, code, etc...). Une image Docker est immuable, c'est-à-dire qu'elle ne change pas une fois créés. Les containers sont créés à partir de l'image Docker. A noter qu'une image peut-être basée sur d'autres images.
Container
Un container est une instance qui est lancée à partir d'une image Docker. Un container partage le noyau du système hôte, ce qui le rend plus léger qu'une VM. Concrètement, on peut exécuter environ 5 fois plus d'instances d'applications avec un containers, qu'avec une VM sur une machine équivalente. Chaque containers fonctionne dans son propre environnement isolé. Pour cela, on utilise des namespaces et des cgroups Linux.
- Les namespaces permettent de spécifier ce que voit le container. Cela permet d'isoler le processus.
- Les cgroups (control groups) permettent d'indiquer ce que peut faire le container. Cela permet de limiter l'utilisation des ressources (RAM, CPU, réseau, entreés/sorties, disque).
Dockerfile
Le Dockerfile est un fichier texte qui contient des instructions pour construire une image Docker. C'est un peu notre recette de cuisine, chaque ligne du fichier correspond à une étape dans la construction de l'image. Le gros point fort du Dockerfile est qu'il permet une approche Infrastructure as Code, peu importe la machine, on est capable de reproduire à l'identique l'environnement de notre application.
Volumes
Je vous parlez au début de cet article du stockage des données avec Docker et notamment de la notion de volume. Vous l'avez sûrement compris quand on crée un container, on part d'une image Docker. Quand on supprime un container, les données contenues dans le container disparaissent. C'est là que les volumes interviennent, ils permettent de stocker les données de manière persistante en dehors du cycle de vie du conteneur. Exemple un container MySQL perdra toutes les données qui auront été écrites dans la base de données au cours de son cycle vie. C'est pour cela que les volumes sont essentiels.
Docker Compose
Maintenant, que j'ai posé les bases de Docker, je vais vous parler de Docker Compose. Docker Compose est un outil d'orchestration de conteneur. Il permet d'automatiser la création et la configuration des conteneurs Docker. Docker Compose utilise un simple fichier YAML, ce qui le rend très simple d'utilisation et reproductible.
Docker Compose a une approche par service. Un service correspond à un conteneur Docker. Par exemple dans une application Web basique, on va retrouver plusieurs services t'elle qu'un serveur Web, une base de données et un interpréteur (exemple : PHP) et bien avec Docker chacun de ces services sera un conteneur. Cependant, pour que les conteneurs puissent interagir ensemble. On créée des volumes et des réseaux virtuels pour qu'il communique entre eux.
Mon environnement
Maintenant, que vous avez un petit morceau de théorie. Je vais vous présenter l'environnement que j'ai mis en place et comment le reproduire chez vous où l'adapter à vos besoins.
Avant de rentrer dans la technique, petit disclaimer. Il s'agit d'un environnement de développement avec un usage purement local, ce dernier n'a pas vocation à être mis en production t'elle qu'elle ou sur un serveur accessible depuis Internet.
J'ai pu tester mon environnement Docker sur Linux et Windows. Je ne vais pas détailler l'installation de Docker et Docker Compose dans cet article, mais je vous invite à consulter la documentation officielle de Docker qui est très bien faite.
- Windows : https://docs.docker.com/desktop/setup/install/windows-install/
- Linux : https://docs.docker.com/engine/install
Actuellement, j'ai juste créé un environnement pour du développement Web en PHP, ce dernier comporte les services suivants :
- Serveur web Nginx
- PHP-FPM
- PostgreSQL
- Mailpit (pour le test d'envoi de mail)
Vous pourrez retrouver tout le code sur mon Github ici.
Architecture
Mon environnement se compose de 4 fichiers :
- .env => contient mes variables qui sont réutilisées dans docker-compose.yaml
- Dockerfile => contient mon image custom de PHP-FPM
- docker-compose.yaml => fichier docker compose pour créer mes conteneurs
- site.conf => configuration par défaut pour nginx
Mon image PHP
Mon image PHP est basée sur l'image PHP-FPM et comporte un certain nombre de module PHP dont je peux avoir besoin. Voici le code de cette image que je vais vous détailler.
FROM php:8.4-fpm LABEL version="1.0" LABEL description="Image PHP for Pixowk Dev Environment" ## Installation paquets de base RUN apt update RUN apt install -y curl git wget libfreetype-dev libjpeg62-turbo-dev libpng-dev ##PHP Extensions ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ RUN install-php-extensions opcache pdo curl xml mbstring json imap posix intl hash openssl pgsql pdo_pgsql zip ## Composer RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer # Nettoyage du cache RUN apt clean && rm -rf /var/lib/apt/lists/* WORKDIR /var/www/htmlL'instruction FROM permet d'indiquer l'image de base à utiliser (ici php-fpm en version 8.4). J'ai ensuite ajouté deux instructions LABEL, c'est dernières ne sont pas essentiel, mais elles permettent d'indiquer des métadonnées à l'image sous la forme de clés/valeurs.
Comme l'image de base est basé sur Debian, le gestionnaire de paquet est donc APT. Quand on veut exécuter des commandes dans notre Dockerfile, on utilise l'instruction RUN. J'exécute ici un apt update pour mettre à jour les dépôts en cache et apt install pour installer des paquets de bases comme git, wget et des librairies.
Je parlais précédemment de module PHP, afin de simplifier leur installation, j'utilise un outil open source pour docker qui est docker-php-extension-installer. Pour copier un fichier ou un répertoire extérieur dans notre image, on peut utiliser l'instruction ADD ici elle permet de copier la dernière version de docker-php-extension-installer dans /usr/local/bin, le --chmod=0755 permet de positionner les droits en lecture, écriture et exécution pour l'utilisateur propriétaire et en lecture et exécution pour le groupe et les autres utilisateurs. J'installe ensuite avec cet outil mes modules PHP. J'en profite aussi pour installer par la suite composer.
Afin d'optimiser la taille de mon conteneur Docker, j'exécute la commande apt clean && rm -rf /var/lib/apt/lists/* qui permet de vider le cache APT et de nettoyer la liste des paquets.
Pour finir, l'instruction WORKDIR permet d'indiquer le répertoire de travail de mon conteneur. Ici, il correspond au dossier où sera stockée mon application Web.
En vous plaçant dans le même répertoire que votre Dockerfile, vous pouvez exécuter docker build pour construire l'image Docker.
Configuration de mes services avec Docker Compose
Comme dit plutôt dans cet article Docker Compose permet d'automatiser la création et la configuration des conteneurs Docker avec un simple fichier YAML. Pour mieux comprendre le fonctionnement de Docker Compose, voici mon fichier docker-compose.yaml que nous allons détailler ensemble.
services:
## NGINX
nginx:
image: nginx:stable
container_name: nginx
ports:
- 80:80
- 443:443
volumes:
- ./app:/var/www/html
- ./site.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
#PHP
php:
build: .
image: pixwok-php
container_name: php
volumes:
- ./app:/var/www/html
- ./php.ini:/usr/local/etc/php/conf.d/php.ini
depends_on:
- db
#DB PostgreSQL
db:
image: postgres:17.6
container_name: postgresql
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB: ${POSTGRES_DB}
TZ: ${POSTGRES_TZ}
PGTZ: ${POSTGRES_PGTZ}
volumes:
- ./db_data:/var/lib/postgresql/data
## Mailer
mailer:
image: axllent/mailpit
ports:
- "1025"
- 8025:8025
environment:
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
L'architecture de Docker Compose est plutôt simple à comprendre 1 service égal 1 conteneur.
Un fichier docker-compose.yaml commence toujours par services: puis on va lister tous nos services et leur config associé. On retrouve bien nos 4 services, chaque service utilise une image qui est indiquée de la façon suivante :
service:
mon-service:
image: postgres:17.6
Comme vous le voyez, chaque service appelle une image Docker qui sera téléchargée au moment de l'exécution de docker compose up. On peut ici préciser la version de l'image avec son tag. C'est ce que j'ai fait pour mon image postgreSQL. On peut aussi indiquer avec l'instruction build: de construire l'image à partir d'un Dockerfile. C'est le cas pour mon service php, j'indique ici build: . car mon dockerfile est dans le même répertoire. J'aurai très bien pu mettre un lien vers un dépôt Github ou vers un autre dossier. Juste après image, j'ai indiqué le paramètre container_name qui permet de donner un nom à mes conteneurs une fois créé. Cela n'est pas obligatoire, c'est clairement pour simplifier leur gestion par la suite.
Pour plusieurs de mes services, on retrouve les paramètres ports et volumes.
Le paramètre ports permet de rendre accessible le port d'un conteneur depuis l'hôte. Exemple :
ports:
- 80:80
- 443:443
Pour mon conteneur Nginx, je rends accessible par l'hôte les ports 80 et 443. Dans cette syntaxe, le premier numéro est le port sur l'hôte et le second le port du conteneur. Les deux peuvent être différents.
Le paramètre volumes permet de configurer des volumes qui peuvent être partagés entre les différent conteneur. Il permet aussi de rendre pérennes les données même après la fin du cycle de vie des conteneurs. Comme pour les ports, le premier chemin avant les deux points est sur la machine hôte le second dans le conteneur. Exemple sur mon conteneur Nginx :
volumes:
- ./app:/var/www/html
- ./site.conf:/etc/nginx/conf.d/default.conf
Ici le répertoire app est mappé avec /var/www/html de mon conteneur, cela signifie que tout le code que je vais déposer dans le dossier app sur ma machine, sera vu par mon conteneur comme étant dans /var/www/html. De même pour mon fichier sites.conf qui sera mappé avec le fichier default.conf situé sous /etc/nginx/conf.d/ sur mon conteneur. Cela me permettra de modifier mon fichier de configuration nginx depuis ma machine hôte, sans avoir à reconstruire mon image et mon conteneur.
Pour mes services mailer et db, on retrouve un paramètre nommé environment ce dernier permet de passer des paramètres au service. Effectivement certaines images Docker dispose de variables d'environnement qui peuvent être défini à la création du conteneur. Par exemple pour mon conteneur PostegreSQL, je lui ai indiqué des informations t'elle que le nom de la base, l'utilisateur, le mot de passe et la timezone.
Vous avez sûrement remarqué que certains de mes services disposent du paramètre depends_on, ce paramètre permet d'indiquer au conteneur qu'il ne peut se lancer que si un autre est déjà lancé. Par exemple nginx ne peut démarrer que si php est démarré et php ne peut démarrer que si la base de données est démarrée. Ce paramètre permet alors d'orchestrer l'ordre de démarrage des conteneurs.
Maintenant que notre docker-compose.yaml est prêt, on peut l'exécuter afin de télécharger / créer nos images et lancer nos conteneurs. Pour cela, on utilise la commande suivante.
docker compose up --detach
L'option --detach permet de redonner la main à la fin de l'exécution du docker compose. Voilà nos conteneurs sont maintenant opérationnels.
