Article original : http://carnetdudev.net/2019/07/14/mettre-en-place-un-environnement-de-developpement-docker-pour-symfony-4/ (Joel Patrick Nzdie)

L’un des principaux arguments mis en avant pour l’adoption de Docker est le fait que la technologie permet d’éviter le Syndrome du « ça marchait pourtant sur machine » aux développeurs en leur permettant de développer leurs applications sur un environnement identique ou presque à la production. Lorsqu’on débute avec Docker on peut avoir beaucoup de mal à concevoir comment mettre cela en pratique.

Bref rappel du fonctionnement d’un conteneur docker

Un conteneur docker s’exécute à partir d’une image qui est elle même construite en exécutant une suite de commandes contenues dans un fichier particulier (Dockerfile ou docker-compose.yaml ou tout autre nom de fichier d’ailleurs mais dont le contenu est sous yaml). Dans le cas d’une application qu’on développe, les différentes étapes lors de la construction de l’image peuvent être les suivantes:

  • Téléchargement de l’image de base
  • Installation des paquets
  • Copie des fichiers locaux vers l’image

Le problème

Au vu de tout celà, celui qui débute va se dire: « tient à chaque fois que je ferais une mise à jour dans mon projet, je vais devoir: reconstruire l’image puis relancer le conteneur? Juste pour tester une légère mise à jour par exemple » Celà est impensable.

Une image par définition est statique, et une fois qu’on a exécuté un conteneur à partir d’une image donnée, il n’est plus possible de mettre à jour le contenu de ce dernier (en pratique on peut se connecter au conteneur y apporter des modifications mais cela évidement est impensable dans un workflow de développement).

Ce qu’on aimerai c’est que à chaque fois qu’on met à jour notre projet, les mises à jour soient immédiatement propagées au conteneur dans lequel notre application s’exécute.

La solution: Les volumes Docker

Vous vous doutiez bien que les concepteurs de Docker (des développeurs 🙂 ) ont pensé à cela, même si au fond le but des volumes n’était pas de répondre à cette problématique, leur utilisation permet de résoudre ce problème.

Comme vous le savez, par défaut l’environnement d’un conteneur est isolé de l’extérieur (son hôte), on n’y communique qu’à travers les ports qui sont spécifiquement mappés lors du lancement du conteneur. Les volumes eux, permettent au conteneur de faire le mapping entre les répertoires présents en interne et ceux présents sur l’hôte. Pour exécuter un conteneur en lui précisant des volumes à mapper avec l’extérieur vous pouvez utiliser la commande:

docker run -v repertoire_sur_hote:repertoire_dans_le_conteneur nom_image

Pour plus d’informations consultez le site officiel https://docs.docker.com/storage/volumes/

Mise en pratique

Nous allons ici mettre en place un environnement de développement pour une application Symfony. L’application utilisera une base de données mysql

Docker Compose VS Docker en mode DEV

Docker compose compose signifie littéralement « composition de docker ». Pour faire simple, lorsqu’on doit travailler avec un seul conteneur, on peut se contenter d’un fichier Dockerfile et des commandes en ligne Docker (Docker CLI). Dans la pratique une véritable application fera appel à plusieurs conteneurs, exécuter chacun de façon individuelle peut devenir très rapidement fastidieux et donc inadapté à un workflow de développement. Docker compose permet donc de gérer l’ensemble des containers dont on dispose comme un tout.

Initialisation du projet

Créons tout d’abord notre projet « tutoriel » comme indiqué dans la documentation officiel (https://symfony.com/doc/current/setup.html) pour notre projet et initialisons un projet web comme indiqué dans la documentation officielle de Symfony.

composer create-project symfony/website-skeleton tutoriel

Une fois la commande exécutée on devrait avoir la structure du projet initiale ci-dessous.

Structure initiale du projet
Structure initiale du projet

Notre fichier docker-compose.yml

version: '3'
services:
  tutoriel:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
    volumes:
      - /var/www/html
      - .:/var/www/html
  db:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_DATABASE: 'tutoriel'
      # So you don't have to use root, but you can if you like
      MYSQL_USER: 'tutoriel'
      # You can use whatever password you like
      MYSQL_PASSWORD: 'tutoriel'
      # Password for root access
      MYSQL_ROOT_PASSWORD: 'tutoriel'
    ports:
      # <Port exposed> : < MySQL Port running inside container>
      - '3309:3306'
    expose:
      # Opens port 3306 on the container
      - '3306'
      # Where our data will be persisted
    volumes:
      - ./data/dump:/docker-entrypoint-initdb.d
      - ./data/db:/var/lib/mysql

Notre fichier docker-compose contient 2 services, le service tutoriel qui est notre application et le service db, notre base de données.

Le service tutoriel

Le service tutoriel est chargé de l’exécution de notre application, il utilise pour cela le conteneur définit dans le fichier Dockerfile.dev (C’est une convention personnelle de naming des fichiers docker lorsque je suis en développement).

Ce qu’il faut remarquer dans ce fichier, et c’est là que réside toute la magie, c’est cette section:

volumes:
      - /var/www/html
      - .:/var/www/html

Dans la section ci-dessus on défini un mapping de répertoires, entre le répertoire courant (.) et un répertoire qui se trouve dans le conteneur (/var/www/html), avec ce mapping toutes mises à jour de fichier en local excepté les mises à jour faites sur le dossier /var/ww/html/vendor seront automatiquement prises en compte par le conteneur sans qu’on ait besoin de reconstruire l’image. Le contenu du fichier Dockerfile.dev est donné ci-dessous.

FROM ubuntu:18.04

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -yq --no-install-recommends \
    apt-utils \
    curl \
    # Install git
    git \
    # Install apache
    apache2 \
    # Install php 7.2
    libapache2-mod-php7.2 \
    php7.2-cli \
    php7.2-json \
    php7.2-curl \
    php7.2-fpm \
    php7.2-gd \
    php7.2-ldap \
    php7.2-mbstring \
    php7.2-mysql \
    php7.2-soap \
    php7.2-sqlite3 \
    php7.2-xml \
    php7.2-zip \
    php7.2-intl \
    php-imagick \
    # Install tools
    openssl \
    nano \
    graphicsmagick \
    imagemagick \
    ghostscript \
    mysql-client \
    iputils-ping \
    locales \
    sqlite3 \
    ca-certificates \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

# Install composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

# Set locales
RUN locale-gen en_US.UTF-8 en_GB.UTF-8 de_DE.UTF-8 es_ES.UTF-8 fr_FR.UTF-8 it_IT.UTF-8 km_KH sv_SE.UTF-8 fi_FI.UTF-8

WORKDIR /var/www/html

COPY composer.json .

RUN composer install

RUN  php bin/console cache:clear

RUN  php bin/console assets:install

CMD php bin/console server:run 0.0.0.0:8000

Le service db

Le service db est notre base de données, ici la section du fichier docker-compose.yml qui nous intéresse est la suivante:

volumes:
      - ./data/dump:/docker-entrypoint-initdb.d
      - ./data/db:/var/lib/mysql

Cette section mérite en effet quelque explications. Pour rappel, par défaut lorsqu’un conteneur est arrêté, aucune donnée générée lors de son exécution n’est enregistrée. Pour notre base de données cela signifie que chaque fois que nous allons vouloir utiliser notre BD, si nous avions préalablement arrêté le conteneur, il faudra recréer toutes nos données. Avec cette section du fichier docker-compose les données de notre BD seront enregistrées dans le repertoire data/db qui se trouve à la racine de notre projet et le point d’entrée de mysql sera dans le repertoire data/dump lui aussi à la racine du projet, nous garantissant ainsi la durabilité de nos données.

Créons donc un repertoire data à la racine du projet et à l’intérieur de celui-ci deux autres repertoires dump et db

data
  dump
  db

Communication entre nos deux services

Notre application a besoin de se connecter à la base de données, les deux services étant définis dans le fichier docker-compose.yaml, il nous suffit de mettre à jour le fichier .env qui est à la racine du projet et remplacer la ligne de configuration de l’url de connexion par défaut (mysql://db_user:db_password@127.0.0.1:3306/db_name) par mysql://tutoriel:tutoriel@db:3306/tutoriel.

Comme vous pouvez le constater, en lieu et place de l’adresse ip (127.0.0.1) on renseigne plutôt l’id du service (db) en interne docker-compose sait comment rediriger vers la base de donnée. De façon général lorsqu’un service définit dans le fichier docker-compose veut contacter un autre cela se fait via l’id du service qui sert d’adresse IP dans ce cas.

Passons à l’exécution

Etant à la racine du projet exécutons la commande ci-dessous

docker-compose up --build

Si tout se déroule bien on devrait avoir dans la sortie de notre écran un contenu de ce genre

votre-home$ docker-compose up --build
Building tutoriel
Step 1/11 : FROM ubuntu:18.04
 ---> 7698f282e524
Step 2/11 : ENV DEBIAN_FRONTEND=noninteractive
 ---> Using cache
 ---> ed5548932915
Step 11/11 : CMD php bin/console server:run 0.0.0.0:8000
 ---> Using cache
 ---> 9d50d51ad0c3
..................................................................................................................................................................................................................


Testons ensuite que notre application fonctionne bien. Si tout s’est bien passé en visitant l’url http://localhost:8000 vous devrez avoir le résultat ci-dessous:

Rajoutons une fonctionnalité à notre application

Créons un nouveau contrôleur dans notre application (src\Controller\HelloContorller.php)

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;




class HelloController extends AbstractController
{

    /**
     * @Route("/hello", name="app_hello")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
       return Response::create("Hello world");
    }

}

Naviguons sur la nouvelle route que nous avons crée http://localhost:8000/hello. On obtient alors le résultat ci-dessous

Faisons une petite mise à jour à notre contrôleur, au lieu de retourner « Hello World » nous allons retourne « Hello world! How are you doing? ». Enregistrons les modifications et relançons la page. On voit que les modifications ont été prises en compte dans le conteneur.

Où se placer pour executer les commandes symfony

Où se placer pour executer les commandes symfony?

Afin d’exécuter les commandes Symfony, on a deux alternatives:

Se connecter au container

Tout d’abord il faut avoir l’id de notre container ensuite execute la commande

docker exec -it id_container bash

Une fois dans le container on se déplace dans le répertoire du projet (/var/www/html/ dans notre cas), on peut alors exécuter toutes nos commandes.

Exécuter les commandes à partir de la machine hôte

En utilisant toujours docker exec en étant sur l’hôte on peut executer les différentes commandes Symfony.

Exemple: créer une migration

docker exec -it id_container php /var/www/html/bin/console make:migration

Comme vous pouvez le voir, à la commande docker exec on passe notre commande classique php bin/console, on a juste besoin de préciser le repertoire du projet à chaque fois.

Conclusion

Si vous êtes arrivés jusqu’ici, vous êtes désormais capables de mettre en place un environnement de développement Symfony utilisant docker. Vous pouvez consulter le repository du tutoriel sur github qui contient en plus un fichier Dockerfile à utiliser lorsque vous souhaiterez déployer votre application en production. Le lien c’est par ici https://github.com/ndziePatrickJoel/docker-symfony-setup