Conteneurisation Docker d’un projet PHP

Aujourd’hui, nous allons monter un environnement de travail complet sous Docker pour un projet PHP. Nous utiliserons des conteneurs Docker, un fichier de variable d’environnement et un Makefile pour automatiser le tout.

Toutes ces manipulations ont été réalisées sous Ubuntu 22 LTS et PhpStorm 2023.1

Mise en route

On démarre avec un dossier vide histoire de commencer dans le propre.

cd projets

mkdir monSuperProjet

Là, on va mettre en place notre structure Docker. On créé un nouveau fichier nommé docker-compose.yml. Il va contenir les conteneurs de tous nos serveurs : Apache, base de données et éventuellement d’autres services annexes.

# fichier /docker-compose.yml
version: "3"

services:
  webserver:
    image: php:8.2-apache
    ports:
      - "80:80"

Pour l’instant, on ne fait que le minimum pour s’assurer que ça fonctionne. Nous allons fonctionner par itérations et enrichir au fur et à mesure.

On lance le docker compose avec la commande :

docker compose up -d

[+] Running 2/2
✔ Network skeleton-docker-cakephp_default Created 0.1s
✔ Container skeleton-docker-cakephp-webserver-1 Started 0.6s

Et on vérifie avec le navigateur sur http://localhost (attention, nous ne sommes pas en httpS). Boom, le serveur Apache est bien disponible ! Bon avec un message d’erreur, mais il répond.

Le conteneur Apache est accessible sous Firefox à l'adresse http://localhost.
Le conteneur Apache est accessible via Firefox à l’adresse http://localhost.

Une autre manière de le vérifier est d’utiliser Portainer (dans le module Containers) ou de lancer docker ps dans le terminal.

Commandes rapides grâce au fichier Makefile

Maintenant, on va tâcher de réduire un peu le nombre de commande à taper. Parce que docker compose up -d, c’est court, mais quand on va rajouter une tripotée de paramètres derrière, ça va vite nous faire chier. Et encore, le docker compose automatise le build s’il n’existe pas.

Donc, on créé un fichier nommé Makefile (sans extension) à la racine, au même niveau que le docker-compose.yml.

# fichier /Makefile

build:
	docker-compose build

clean:
	docker-compose kill
	docker-compose down --volumes --remove-orphans

start:
	docker-compose up -d

stop:
	docker-compose stop

L’idée est de taper la commande make, suivie du nom de la commande définie.
Par exemple, pour démarrer le tout:

make start

Ou pour arrêter, on tape: make stop. Vous avez compris le principe.

Maintenant, on va enrichir le Makefile en combinant plusieurs commandes: on ajoute la ligne suivante en fin de fichier:

reset: stop clean build

On teste la nouvelle commande:

make reset

docker-compose stop
Stopping skeleton-docker-cakephp-webserver-1 … done
docker-compose kill
docker-compose down –volumes –remove-orphans
Removing skeleton-docker-cakephp-webserver-1 … done
Removing network skeleton-docker-cakephp_default
docker-compose build
webserver uses an image, skipping

Et voilà ! On a donc en une seule commande simple, arrêté, nettoyé et recréé le conteneur. Et ce qui est génial en la combinant avec le docker compose ; c’est que ça fonctionnera quelque soit le nombre de conteneur !


Allez, on continue. On va enrichir le Makefile avec l’inclusion d’un fichier .env.local pour utiliser des variables d’environnement.

# fichier /Makefile

include .env.local


build:
	 docker-compose --env-file .env.local build

clean:
	docker-compose --env-file .env.local kill
	docker-compose --env-file .env.local down --volumes --remove-orphans

reset: stop clean build start

restart: stop start

start:
	docker-compose -p ${APP_NAME} --env-file .env.local up -d
	@echo Projet ${APP_NAME} disponible sur http://localhost

stop:
	docker-compose --env-file .env.local stop

Et le fichier .env.local, toujours à la racine.

# fichier /.env.local,
#
#           /!\ --- NE PAS COMMIT !!! ---
#
APP_NAME=monSuperProjet
PORT_WEB="80"

Mais, c’est une mauvaise pratique de commiter les fichiers d’environnement !

Ah, oui, c’est vrai que ces fichiers sont spécifiques à la configuration du serveur qui héberge les containers. On va contourner le problème en utilisant un fichier .env.exemple à commiter sur le dépôt.

# fichier /.env.exemple
# contenu à dupliquer dans un fichier .env.local
#
#           /!\ --- NE PAS COMMIT !!! ---
#
APP_NAME=monSuperProjet

Le fichier README.md est complété en ce sens.

### fichier /README.md
#
# Skeleton Docker CakePHP
# ZionLabs 2023


## Démarrage rapide TL;DR

```
mkdir skeleton-docker-cakephp  && cd $_
cp .env.exemple .env.local
make reset
```

Configuration Apache

On va maintenant corriger l’accès à la page d’accueil. On ajoute le fichier de configuration docker/webserver/conf_vhosts/default.conf :

# fichier /docker/webserver/conf_vhosts/default.conf
<VirtualHost *:80>
    ServerAdmin webmaster@localhost
    DocumentRoot "/var/www/html"
    ServerName localhost
	<Directory "/var/www/html/">
		AllowOverride all
	</Directory>
</VirtualHost>

Puis on le branche dans le fichier docker-compose, dans notre service nommé webserver :

  webserver:
    image: php:8.2-apache
    container_name: "${APP_NAME}-web"
    ports:
      - '${PORT_WEB}:80'
    volumes:
      - ./docker/webserver/conf_vhosts:/etc/apache2/sites-enabled
      - ./src:/var/www/html

A noter, qu’on y a aussi ajouté le lien vers le fichier PHP suivant:

<!-- fichier /src/index.php -->

<?php
echo "Bonjour le Monde !";

un coup de make reset dans le terminal et on recharge la page du navigateur sur http://localhost.

Conteneur de la base de données

Nous allons déjà être obligés de rajouter le package mysql au serveur Apache pour la connexion au conteneur MariaDB. Pour cela, on va passer par un Dockerfile dédié (en lieu et place de la ligne image: php:8.2-apache ). On ajoute aussi notre service database. Le docker-compose.yml est donc modifié comme suit :

# fichier /docker-compose.yml
version: "3"

services:
  webserver:
    build:
      context: ./docker/webserver
    container_name: "${APP_NAME}-web"
    depends_on:
      - database
    env_file:
      - .env.local
    ports:
      - '${PORT_WEB}:80'
    volumes:
      - ./src:/var/www/html
      - ./docker/webserver/conf_vhosts:/etc/apache2/sites-enabled

  database:
    image: mariadb:latest
    container_name: "${APP_NAME}-db"
    environment:
      MYSQL_RANDOM_ROOT_PASSWORD: 1
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD} #root password show in log in: db_1   | GENERATED ROOT PASSWORD: ...
    ports:
        - '${PORT_DB}:3306'

Et le Dockerfile à placer dans les bons dossiers (cf arborescence en première ligne):

# fichier /docker/webserver/Dockerfile
FROM php:8.2-apache

# Install PHP MYSQL extension and restart server
RUN apt-get clean \
    && apt-get update \
    && docker-php-ext-install mysqli;

Le fichier .env.local est aussi mis à jour :

# fichier /.env.exemple
# contenu à dupliquer dans un fichier .env.local
#
#           /!\ --- NE PAS COMMIT !!! ---
#
APP_NAME=monSuperProjet
DB_NAME=dev
DB_USER=dev
DB_PASSWORD=dev123
PORT_DB=3306
PORT_WEB=80

Et enfin on teste notre connexion dans le fichier src/index.php qui est remanié pour l’occasion:

<!-- fichier /src/index.php -->

<h1>Test de connexion à la base de données</h1>
<em>/!\ il faut patienter 5-10 secondes que le conteneur de base de données soit disponible</em>
<hr style="margin:1em 0 2em">
<?php
$nomServeur     = getenv('APP_NAME').'-db';
$utilisateurBdd = getenv('DB_USER');
$motDePasseBdd  = getenv('DB_PASSWORD');


// création de la connexion
$connexion = new mysqli($nomServeur, $utilisateurBdd, $motDePasseBdd);

// Vérification de la connexion
if ($connexion->connect_error) {
    die("Échec de la connexion: " . $connexion->connect_error);
}
echo "Connexion réussie";

Voilà on reste le tout avec un make reset et ça fonctionne (actualiser la page au bout de 5-10 secondes car le conteneur de base de données n’est pas disponible de suite).

Commentaires dans le Makefile

Bon histoire de peaufiner, on ajoute des commentaires qui commencent par ## et la commande help pour les extraire (merci à Software Carpentry pour la syntaxe).

# fichier /Makefile
include .env.local

## build			construit tous les conteneurs
build:
	 docker-compose --env-file .env.local build

## build-nocache		construit tous les conteneurs (suppression du cache)
build-nocache:
	 docker-compose --env-file .env.local build --no-cache

## clean			arrête et supprime les conteneurs
clean:
	docker-compose --env-file .env.local kill
	docker-compose --env-file .env.local down --volumes --remove-orphans


## help			extrait les commentaires qui commencent par "##" (crédit Software Carpentry)
help : Makefile
	@sed -n 's/^##//p' $<

## reset			arrête, supprime, reconstruit et relance les conteneurs
reset: stop clean build start

## reset-nocache		arrête, supprime, reconstruit et relance les conteneurs (suppression du cache)
reset-nocache: stop clean build-nocache start

## restart		redémarre tout
restart: stop start

## start			démarre tous les conteneurs (et les construit si besoin)
start:
	docker-compose -p ${APP_NAME} --env-file .env.local up -d
	@echo Projet ${APP_NAME} disponible sur http://localhost

## stop			arrête tous les conteneurs
stop:
	docker-compose --env-file .env.local stop

Il suffit donc de taper make help pour afficher toutes les commandes disponibles:

make help

build construit tous les conteneurs
build-nocache construit tous les conteneurs (suppression du cache)
clean arrête et supprime les conteneurs
reset arrête, supprime, reconstruit et relance les conteneurs
reset-nocache arrête, supprime, reconstruit et relance les conteneurs (suppression du cache)
restart redémarre tout
start démarre tous les conteneurs (et les construit si besoin)
stop arrête tous les conteneurs

Conclusion

Voilà, cet article est bien assez long. J’espère qu’il vous sera utile.
Vous pouvez retrouver le dépôt de ce projet ici.

Kevin
Développeur web fullstack, j'ai souvent la tête dans le cloud. Après une dizaine d'année d'expérience en entreprise, j'ai choisi la voie de l'indépendance en créant Zion Labs.