Comment dockeriser une application Symfony ?

Publié le

27 mars 2023

Le but de cet article est de présenter un guide simple étape par étape pour faire fonctionner une application Symfony sur des conteneurs Docker. Ci après la liste de ce qui est nécessaire pour que notre application soit opérationnelle :

  • Un serveur HTTP pour servir nos pages (ex : Apache ou Nginx)
  • Une base de données SQL pour stocker les données de notre application (ex : MySQL, MariaDB, PostgreSQL etc.)
  • Une instance de PHP FPM pour gérer les scripts PHP et communiquer avec le serveur HTTP.

Ces besoins sont probablement les plus communs si vous souhaitez développer une application Symfony.

Pourquoi nous recommandons d'utiliser Docker pour votre application Symfony ?

Parmis les avantages notables qu'il existe, dockeriser votre app symfony permet entre autres :

  • de réduire drastiquement le temps pour passer d'un projet à un autre
  • de ne plus avoir à gérer plusieurs versions de chaque librairie sur votre machine. (Par exemple, si vous travaillez sur deux projets qui utilisent des versions de PHP 5.6 et PHP 8.2)
  • de réduire largement le temps nécessaire pour l'installation du projet à l'arrivée d'un nouveau développeur
  • de permettre de reproduire l'infrastructure de production (Mêmes librairies et versions)
  • de permettre de lancer les tests sur Docker directement depuis la solution de CI
  • de faciliter le déploiement dans différents environnements (staging, préproduction, production)
  • de ne plus avoir des tests fonctionnant sur votre machine locale mais qui échouent sur la CI pour diverses raisons.

Première étape : Installer Docker

La première chose à faire est l'installation de Docker sur votre ordinateur.

Nous avons besoin de :

  • Docker
  • Docker compose

Docker apporte un moyen simplifié pour encapsuler nos logiciels dans des conteneurs qui apportent tout le nécessaire afin de les lancer : code, exécutables, outils systèmes et librairies.

Docker compose est un outil pour définir et exécuter des applications nécessitant plusieurs conteneurs. C'est utilisé principalement pour permettre à nos conteneurs de communiquer entre eux via un réseau privé et partager leur propre système de fichiers via des volumes.

Installation

Si vous êtes sur un système basé sur Ubuntu, vous aurez juste besoin de faire :

apt-get install docker docker-compose 

Voir https://docs.docker.com/install/ & https://docs.docker.com/compose/install/ pour la documentation complète.

Et c'est … fini. Vous avez tout ce qu'il vous faut sur votre ordinateur.

Note: Il s'agit ici de l'installation pour docker compose v1. Pour utiliser docker compose v2, veuillez vous référer à : https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04

Deuxième étape : Organiser la structure du projet

    - apps/
       - my-symfony-app/ [← Le dossier contenant votre application Symfony]
     - bin/
     - docker/
     - .env
     - docker-compose.yaml

Important : Cette structure de dossiers est propre à ce que nous faisons sur nos projets à KNP Labs. Ce n'est basé sur aucune convention et c'est à vous de voir si vous souhaitez la suivre ou non. Il faut juste savoir que les prochaines étapes se baseront sur cette architecture et que vous devrez adapter votre code pour suivre votre propre architecture.

Troisième étape : Le fichier docker-compose

Vous allez avoir besoin d'un fichier listant toutes les images requises pour votre application. À la racine de votre project, créez un fichier "docker-compose.yaml".

Maintenant, nous allons énumérer tout ce que l'on a besoin dans notre application Symfony.

Mettre en place la base de données

Tout d'abord, il nous faut un moteur de base de données. Prenons MySQL. Vous pouvez bien sûr utiliser Postgres ou MariaDB.

Essayons de créer notre fichier.

Vous pouvez copier le code ci-après :

version:  '3.8'
services:
    mysql:
        image: mysql:8.0
        restart: on-failure
        environment:
            MYSQL_RANDOM_ROOT_PASSWORD: "yes"
            MYSQL_DATABASE: example
            MYSQL_PASSWORD: password
            MYSQL_USER: user

Qu'avons nous fait ?

La première ligne spécifie la version de la syntaxe docker compose. Ensuite nous ajoutons la liste des services nécessaire à notre application. C'est une liste d'images Docker. Vous pouvez voir toutes les images disponibles sur Docker Hub : https://hub.docker.com/.
Nous avons juste ajouté la dernière version de MySQL. Nous avons aussi mis en place les variables d'environnement nécessaires comme le mot de passe de l'utilisateur ainsi que le nom de la base de données. Toutes les options sont disponibles ici : https://hub.docker.com/_/mysql

Pour ceux qui veulent avoir une interface web, vous pouvez facilement ajouter Adminer. Ajoutez le code suivant :

version: '3.8'
services:
    mysql:
        …
    
    adminer:
        image: adminer
        restart: on-failure
        ports:
            - '8080:8080'

La partie "Ports" est utile pour gérer la communication entre vos conteneurs et votre ordinateur. Nous relions le port 8080 de notre conteneur vers celui de votre machine. Ainsi vous pourrez accéder à Adminer en accédant au lien suivant : http://localhost:8080/

Configurer le serveur HTTP

Maintenant que l'on a un moteur de base de données, il nous faut un serveur HTTP comme Apache ou Nginx. Pour cet exemple, nous utiliserons Nginx.

Dans votre fichier docker-compose, vous pouvez ajouter :

version: '3.8'
services:
    mysql:
        …
    
    nginx:
        image: nginx:1.23.3-alpine
        restart: on-failure
        volumes:
            - './apps/my-symfony-app/public/:/usr/src/app'
        ports:
            - '80:80'

Comme vous pouvez le voir, il y a plus d'instructions que dans la configuration MySQL.

Vous aurez peut-être remarqué que nous spécifions une version de Nginx, "alpine". Vous pouvez choisir une autre version sur Docker Hub, mais nous préférons utiliser Alpine car il s'agit d'une version très légère de Linux, cela permettra de réduire la consommation de ressources sur votre ordinateur ainsi que le temps de construction des images sur la CI.

Nous allons aussi lier le port 80 de notre ordinateur (le port HTTP par défault), ainsi notre application pourra être accessible sur http://localhost.

Nous utilisons des volumes. Les volumes sont utilisés pour partager une partie du système de fichiers entre l'ordinateur et le conteneur. C'est particulièrement utile en environnement de développement quand on fait constamment des changement dans le code source. Lorsque le conteneur et la machine partagent le code via des volumes, le conteneur n'a pas besoin d'être recréé à chaque fois qu'un changement a lieu.

Dans cet article, nous utilisons une application Symfony 6, nous devons donc partager le dossier public. Si nous étions sur une application Symfony 3, nous devrions partager le dossier web. Par convention, nous stockons le code source sur le conteneur dans /usr/src/app. C'est pourquoi nous avons ajouté la ligne : ./public/:/usr/src/app.

Nous devons à présent indiquer à Nginx comment servir les fichiers de notre projet. La configuration proposée ici est basée sur celle décrite dans la documentation de Symfony

https://symfony.com/doc/current/setup/web_server_configuration.html.

À la racine de notre application, créer un dossier "docker", dans le dossier, créez un sous-dossier "nginx". Vous pouvez à présent ajouter le fichier de configuration nommé "default.conf".

# ./docker/nginx/default.conf
server {
    server_name domain.tld www.domain.tld;
    root /var/www/project/public;

    location / {
        # try to serve file directly, fallback to index.php
        try_files $uri /index.php$is_args$args;
    }

    location ~ ^/index\.php(/|$) {
        fastcgi_pass unix:/var/run/php/php-fpm.sock;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        include fastcgi_params;
    }

    location ~ \.php$ {
        return 404;
    }

    error_log /var/log/nginx/project_error.log;
    access_log /var/log/nginx/project_access.log;
}

Pour une application Symfony 3, vous devez changer "public" en "web" et "index.php" en "app.php".

Nous aurons besoin de donner ce fichier de configuration à notre conteneur Nginx. On peut se servir d'un volume pour partager le fichier de configuration.

Ajouter cette ligne dans le fichier docker-compose.yaml :

    - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'

Votre service nginx devrait ressembler à ceci :

    nginx:
        image: nginx:1.23.3-alpine
        restart: on-failure
        volumes:
            - './apps/my-symfony-app/public:/usr/src/app'
            - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
        ports:
            - '80:80'

Nginx est à présent configuré.

Configuration du serveur PHP

Nous avons presque terminé l'écriture de notre fichier docker-compose.

Pour PHP, on peut ajouter les lignes suivantes :

    php:
        build:
            context: .
            dockerfile: docker/php/Dockerfile
        volumes:
            - './apps/my-symfony-app/:/usr/src/app'
        restart: on-failure
        env_file:
            - .env
        user: 1000:1000

Dans ce conteneur, nous partageons le contenu de notre application dans le dossier /usr/src/app. La configuration de Nginx est déjà prévue pour utiliser ce répertoire donc il n'y a rien d'autre à faire.

Dernière modification à opérer dans la définition de notre service Nginx :

version: '3.8'
services:
    mysql:
        …

    nginx:
        image: nginx:1.23.3-alpine
        restart: on-failure
        volumes:
            - './apps/my-symfony-app/public/:/usr/src/app'
            - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
        ports:
            - '80:80'
        depends_on:
            - php

Nous indiquons juste que le conteneur nginx doit être construit après le conteneur PHP.

Dernier point: Quel est l'intérêt de la ligne user: 1000:1000 ?

Cette ligne permet d'indiquer à notre service quel utilisateur va être utilisé en interne. Ici l'uid et gid 1000. Dans la plupart des cas l'utilisateur de base sur votre machine locale a comme uid 1000 et gid 1000.

Si vous voulez vérifier votre uid, exécuter la commande :

$ id

En ajoutant cette ligne, le conteneur et votre système vont intéragir avec les fichiers (grâce aux volumes) avec le même utilisateur (uid) et ainsi prévenir des problèmes de permissions et de droits.

Maintenant, il nous reste une chose à faire. Comme vous pouvez le voir, comparé aux autres services, il n'y a pas d'image. Nous allons devoir lui fournir un Dockerfile, voyons ensemble comment l'écrire.

Quatrième étape : Le DockerFile

Le Dockerfile est utilisé pour définir des instructions sur nos images. Vous pouvez par exemple installer des extensions PHP ou exécuter des commandes Unix.

Dans cet exemple, nous allons créer un sous-dossier "php" dans le dossier "docker". Dans ce sous-dossier, nous créerons un fichier "Dockerfile".

# ./docker/php/Dockerfile

FROM composer:2.4.2 as composer

##################################

FROM php:8.1-fpm-alpine3.16
    
RUN apk add --no-cache \
    bash=~5.1 \
    git=~2.36 \
    icu-dev=~71.1

RUN mkdir -p /usr/src/app \
    && apk add --no-cache --virtual=.build-deps \
        autoconf=~2.71 \
        g++=~11.2 \
    && docker-php-ext-configure intl \
    && docker-php-ext-install -j"$(nproc)" intl pdo_mysql \
    && pecl install apcu \
    && docker-php-ext-enable apcu intl \
    && apk del .build-deps

WORKDIR /usr/src/app

COPY apps/my-symfony-app/composer.json /usr/src/app/composer.json
COPY apps/my-symfony-app/composer.lock /usr/src/app/composer.lock
    
RUN PATH=$PATH:/usr/src/app/vendor/bin:bin

COPY --from=composer /usr/bin/composer /usr/bin/composer

RUN composer install --no-scripts

COPY apps/my-symfony-app /usr/src/app

RUN chown -R 1000:1000 /usr/src/app
USER 1000:1000

Nous utiliserons donc cette image Docker : “php:8.1-fpm”.

Nous installerons toutes les librairies PHP nécessaires au fonctionnement de notre application Symfony.

Afin de rendre plus lisible nos instructions, nous indiquerons à Docker de nous placer dans “/usr/src/app”.

Ensuite nous copierons l'intégralité de notre application dans le dossier “/usr/src/app”.

Nous avons maintenant tout le nécessaire pour avoir une application fonctionnelle.

Dernière étape : Lancer l'application

Important : Nous devons être sûr d'avoir libérer tous les ports nécessaire de notre ordinateur. Par exemple, si vous avez déjà une instance de Nginx qui tourne sur votre machine et qui écoute le port 80, veuillez éteindre le service pour pouvoir faire fonctionner votre conteneur.

service  stop  nginx

C'est presque fini ! Cela va être simple et rapide maintenant. Vous n'avez plus qu'à ouvrir un terminal et vous placer à la racine de votre projet.

Exécuter :

docker compose build

Cette commande s'occupera de construire tout ce qui se situe dans le fichier docker-compose.yaml.

Une fois votre application construite, vous pouvez exécuter :

docker compose up -d

Afin d'éteindre vos conteneurs, vous pourrez exécuter :

docker compose stop

Maintenant vous pouvez exécuter toutes les commandes Symfony pour construire votre application, ici quelques exemples :

docker compose exec php composer install
docker compose exec php bin/console doctrine:migrations:migrate
docker compose exec php bin/console doctrine:fixtures:load

Et voilà ! Vous pouvez maintenant accéder à votre application sur : http://localhost/

Pour aller plus loin

Maintenant que nous avons une application fonctionnelle, nous pouvons faire quelques petites optimisations.

Premièrement nous avons mis en dur certains paramètres de notre fichier docker-compose.yaml. Nous allons écrire ces paramètres dans un fichier .env pour ensuite les partager à notre réseau Docker.

Quelles variables devons nous définir ?

  • L'utilisateur Mysql
  • Le mot de passe de l'utilisateur Mysql
  • Le nom de la base de donnnées
  • Tous les ports exposés
  • L'utilisateur que les conteneurs doivent utiliser

Créer un fichier .env à la racine du projet

MYSQL_USER=user
MYSQL_PASSWORD=password
MYSQL_DATABASE=example

NGINX_PORT=80

ADMINER_PORT=8080

LOCAL_USER=1000:1000

Note : Nous vous recommandons fortement de ne pas versionner ce fichier mais de dupliquer ce fichier avec seulement les clés dans un fichier .env.dist. Ainsi chaque développeur pourra changer les variables d'environnement à sa guise.

Maintenant remplaçons les variables codées en dur par les nouvelles variables d'environnement.

version: '3.8'
services:
    mysql:
        image: mysql:8.0
        restart: on-failure
        environment:
            MYSQL_RANDOM_ROOT_PASSWORD: "yes"
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_PASSWORD}
            MYSQL_DATABASE: ${MYSQL_DATABASE}
    
    php:
        build:
            context: .
            dockerfile: docker/php/Dockerfile
        restart: on-failure
        volumes:
            - './apps/my-symfony-app/:/usr/src/app'
        user: ${LOCAL_USER}
    
    nginx:
        image: nginx:1.23.3-alpine
        restart: on-failure
        volumes:
            - './apps/my-symfony-app/public/:/usr/src/app'
            - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
        ports:
            - '${NGINX_PORT}:80′
        depends_on:
            - php
    adminer:
        image: adminer
        restart: on-failure
        ports:
            - '${ADMINER_PORT}:8080'

Maintenant, notre fichier docker-compose.yaml est configuré de telle manière que chaque développeur peut l'appréhender selon ses préférences. Cependant nous pouvons aller encore plus loin !

Si nous souhaitons mettre nos conteneurs en environnement de production. Avons-nous vraiment besoin de partager des volumes et relier les ports des conteneurs à la machine distante ? Non. Nous devons trouver un moyen de placer des différentes instructions dans un endroit séparé. Le fichier docker-compose.override.yaml répond à ce besoin.

Le fichier docker-composer.override.yaml est nativement supporté par docker compose et permet de surchager la configuration de base. Voici le résultat final :

# docker-compose.yaml
version: '3.8'
services:
    mysql:
        image: mysql:8.0
        restart: on-failure
        environment:
            MYSQL_RANDOM_ROOT_PASSWORD: "yes"
            MYSQL_USER: ${MYSQL_USER}
            MYSQL_PASSWORD: ${MYSQL_PASSWORD}
            MYSQL_DATABASE: ${MYSQL_DATABASE}
    
    php:
        build:
            context: .
            dockerfile: docker/php/Dockerfile
        restart: on-failure
        user: ${LOCAL_USER}
    
    nginx:
        image: nginx:1.23.3-alpine
        restart: on-failure
        depends_on:
            - php
    
    adminer:
        image: adminer
        restart: on-failure
# docker-compose.override.yaml
version: '3.8'
services:
    php:
        volumes:
            - './apps/my-symfony-app/:/usr/src/app'
    
    nginx:
        volumes:
            - './apps/my-symfony-app/public/:/usr/src/app'
            - './docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro'
        ports:
            - '${NGINX_PORT}:80'
    
    adminer:
        ports:
            - '${ADMINER_PORT}:8080'

Maintenant nous avons une configuration optimisée de Docker qui peut être utilisée par n'importe quel membre de votre équipe et facilement déployable sur un serveur !

Si vous avez la moindre question, n'hésitez pas à nous contacter sur Twitter @KNPLabs :)

Publié par

Antoine Lelaisant
Antoine Lelaisant

Caen

Front and backend developer with a pref for mobile apps. Loves to share his XP with clients & KNPeers during our trainings.

Lénaïc Couëllan
Lénaïc Couëllan

Lénaic loves doing front and back end development and guess what? He loves even more working hand in hand with our clients, communicating and helping define their needs.

Commentaires