lindev.fr

Aller au contenu | Aller au menu | Aller à la recherche

07 juin 2017

Environnement de développement complet avec Docker

Qu'allons nous voir

docker-logo.jpg Nous allons voir comment mettre sur pied avec Docker un environnement de développement Web complet ( LAMP ) en quelques minutes ( si l'on compte uniquement le temps de déploiement, et non la lecture de ce post. ).

Mais surtout , rendre cet environnement facilement configurable pour qu'il réponde à vos attentes.

Au programme, nous aurons donc 3 conteneurs


Petit plus, nous aurons à disposition, un fichier de configuration permettant l'utilisation de la fonction mail() de php.

Pour lancer facilement ces conteneurs Docker, nous utiliseront l'outil docker-compose, qui permet en un seul fichier de déclarer notre environnements et bien plus encore.

Pour les plus pressés : Si vous ne souhaitez pas créer les répertoires et fichiers de conf à la main en suivant tout le billet, vous pouvez directement cloner le projet puis lancer ( Après avoir installé Docker sur votre machine évidemment ). Rendez-vous tout en bas de cette page, dans la section Go go go.

Installation de Docker et docker-compose

Franchement, il n'y a rien de compliqué, je ne vais pas détailler spécialement la méthode , juste vous donner les liens pour les différents systèmes.
Après, il suffit de suivre.

Docker CE

Docker-Compose

Bonne nouvelle pour les utilisateurs de Mac & Windows, l'outil est automatiquement installé avec Docker.

Pour les utilisateurs Linux, je conseille la commande pip

sudo pip install docker-compose

Le projet LAMP

Parfait, nous avons tout les outils nécessaires pour atteindre notre objectif.
Commençons par créer un répertoire qui contiendra l’ensemble des fichiers de configuration et le code source de nos futurs projets Web .

mkdir -p dockerlamp/www dockerlamp/docker/apache/vhosts dockerlamp/docker/mysql dockerlamp/docker/php

Voici en résumé ce que ça donne :

dockerlamp
├── docker
│   ├── apache
│   │   └── vhosts
│   ├── mysql
│   ├── php
└── www

Nous aurons donc les fichiers de configuration dans le répertoire docker/ et le code source sera dans le répertoire www/

docker-compose.yml

Je vais pas tourner autour du pot, je vais tout de suite vous donner le contenu complet du fichier docker-compose.yml, nous verrons ensuite les différents points à compléter.

Créez donc le fichier docker-compose.yml à la racine du répertoire dockerlamp/

# Adopt version 2 syntax:
#   https://docs.docker.com/compose/compose-file/#/versioning
version: '2'

volumes:
    database_data2:
        driver: local

services:
###########################
# Setup the Apache container
###########################
    httpd:
        container_name: dockerapache_httpd_1
        restart: always
        image: httpd:2.4.25
        ports:
            - 80:80
        volumes:
            - ./docker/apache/httpd.conf:/usr/local/apache2/conf/httpd.conf
            - ./docker/apache/vhosts/:/usr/local/apache2/conf/vhosts
        volumes_from:
            - php

###########################
# Setup the PHP container
###########################
    php:
        container_name: dockerapache_php_1
        restart: always
        build: ./docker/php/
        expose:
            - 9000
        volumes:
            - ./www/:/usr/local/apache2/htdocs
            - ./docker/php/ssmtp.conf:/etc/ssmtp/ssmtp.conf:ro
            - ./docker/php/php-mail.conf:/usr/local/etc/php/conf.d/mail.ini:ro


###########################
# Setup the Database (MySQL) container
###########################
    mysql:
        container_name: dockerapache_mysql_1
        restart: always
        image: mysql:5.6.36
        expose:
            - 3306
        volumes:
            - database_data2:/var/lib/mysql
            - ./docker/mysql/conf-mysql.cnf:/etc/mysql/mysql.conf.d/conf-mysql.cnf:ro
        environment:
            MYSQL_ROOT_PASSWORD: secret
            MYSQL_DATABASE: project
            MYSQL_USER: project
            MYSQL_PASSWORD: project

Configuration Apache

Afin de configurer finement le service Apache, nous allons récupérer le fichier httpd.conf et le déposer dans le répertoire dockerlamp/docker/apache/

Voici le lien de téléchargement httpd.conf

La seule modification faite à ce fichier ( comparé à la version d'origine ) est la prise en charge des virtualhosts.

Nous allons par conséquent également récupérer le virtualhost par défaut et le placer dans le répertoire dockerlamp/docker/apache/vhosts/

Voici le lien de téléchargement 000-default.conf


Pour les curieux, si vous éditez le fichier 000-default.conf, vous verrez que l'on utilise le module proxy d'apache pour gérer les sources php . Nous passons ces sources au service nommé "php" sur le port 9000.

Nous en avons terminé avec le service apache . Passons au service php.

Configuration php-fpm

Contrairement au service apache, le conteneur php-fpm doit être modifié, pour ne serait-ce qu'installer les extensions.
Nous allons donc créer un fichier Dockerfile, qui contiendra toutes les instructions pour finaliser notre container .

La bonne pratique

J'aurais pu simplement créer une image du container sur dockerHub et la mettre à disposition, mais pour partager une image modifiée, la bonne pratique consiste à utiliser comme base l'image officielle ( php ), puis d'ajouter explicitement les changements à apporter dans un DockerFile.

Ce qui vous permet d'être certain que l'image finale n'est pas corrompue.

Dockerfile

créez le fichier Dockerfile dans le répertoire dockerlamp/docker/php/, comme précisé dans docker-compose.yml.

FROM php:5.6-fpm

# install the PHP extensions we need
RUN set -ex; \
	\
	apt-get update; \
	apt-get install -y \
		libjpeg-dev \
		libpng12-dev \
                libmcrypt-dev \
                ssmtp \
	; \
	rm -rf /var/lib/apt/lists/*; \
	\
	docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr; \
	docker-php-ext-install gd mysqli opcache pdo_mysql json mcrypt; \
        pecl install -o -f xdebug redis \
        && rm -rf /tmp/pear \
        && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini \
        && echo "zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20131226/xdebug.so" > /usr/local/etc/php/conf.d/xdebug.ini; 
# TODO consider removing the *-dev deps and only keeping the necessary lib* packages

# set recommended PHP.ini settings
# see https://secure.php.net/manual/en/opcache.installation.php
RUN { \
		echo 'opcache.memory_consumption=128'; \
		echo 'opcache.interned_strings_buffer=8'; \
		echo 'opcache.max_accelerated_files=4000'; \
		echo 'opcache.revalidate_freq=2'; \
		echo 'opcache.fast_shutdown=1'; \
		echo 'opcache.enable_cli=1'; \
	} > /usr/local/etc/php/conf.d/opcache-recommended.ini

RUN { \
                echo '[xdebug]'; \
                echo 'xdebug.default_enable = 0'; \
        } >> /usr/local/etc/php/conf.d/xdebug.ini


RUN { \
        echo 'error_reporting  =  E_ALL'; \
        echo 'log_errors = On'; \
        echo 'display_errors = Off'; \
    } > /usr/local/etc/php/php.ini

CMD ["php-fpm"]
EXPOSE 9000

php7-fpm : Si vous préférez une autre version de php, il vous suffit de la changer sur la première ligne ( ex : FROM php:7.1.5-fpm ).
Toutes les versions sont listées ici.

fonction mail()

Puisque nous utilisons le service php à l'intérieur d'un container, seul ce service est disponible ( bien qu'il soit possible de lancer plusieurs services dans un même conteneur, cette méthode n'est pas conseillée ), le service sendmail n'est pas disponible.

Pour palier à ce problème, nous allons utiliser ssmtp. Cet utilitaire ne fait que rediriger les mails vers un serveur SMTP externe.
Pour le configurer ( l'activer au sein de php et définir quel serveur SMTP utiliser ), nous allons ajouter deux fichiers dans le répertoire dockerlamp/docker/php/

  • php-mail.conf
  • ssmtp.conf
php-mail.conf

Ce fichier de configuration ( chargé comme un php.ini supplémentaire ) ne fait que surcharger le chemin de l’exécutable sendmail afin d'utiliser ssmtp.

[mail function]
sendmail_path = "/usr/sbin/ssmtp -t"
ssmtp.conf

Ce fichier vous permet de définir quel serveur SMTP utiliser pour envoyer vos mails.
Par exemple, pour utiliser un compte Gmail:

mailhub=smtp.gmail.com:587
[email protected]
AuthPass=mypassword

FromLineOverride=YES
hostname=dockerdev.foo.net
UseSTARTTLS=YES

Seules les 3 premières lignes sont à adapter selon votre cas.

Nous en avons terminé avec la configuration de php-fpm. Passons à Mysql

Configuration Mysql

C'est presque le service le plus simple... dans le fichier docker-compose, nous avons défini des variables d'environnements, qui spécifient le mot de passe du compte root, une base de données à créer avec un utilisateur associé (login et mot de passe) .

Par la suite, si vous devez ajouter une nouvelle base de données, deux possibilités .

  1. Ajouter les informations dans le fichier docker-compose, et relancer le conteneur
  2. Se connecter en root sur le service mysql et créer manuellement la base de données et son utilisateur .

Nous allons créer le fichier conf-mysql.cnf, dans le répertoire dockerlamp/docker/mysql/ ce fichier est automatiquement monté dans le conteneur Mysql à son démarrage et permet donc de modifier simplement la configuration de mysql .

Par exemple, si l'on souhaite modifier la variable sql_mode

[mysqld]  
sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"

Note : Vous pouvez laisser ce fichier vide si vous n'en avez pas l'utilité pour le moment. Mais il doit être présent dans le répertoire mysql !

Notre environnement LAMP est maintenant prêt !

Lancer l'environnement LAMP

Puisque nous avons configuré notre environnement via un fichier docker-compose.yml, nous allons évidemment utiliser la commande docker-compose pour le lancer.
Placez-vous à la racine du projet dockerlamp/

docker-compose up -d

Cette commande va construire les volumes, les interfaces réseaux, télécharger les images nécessaires, construire les conteneurs si besoin ( via le Dockerfile ) et enfin ... les exécuter en tache de fond . Oui oui .. tout ça en une seule commande .. le pied!

Le test

Nous allons tester notre stack, apache et php.

Créez deux fichiers dans le répertoire www/

  • info.php
  • index.html
info.php
<?php 
echo phpinfo();
index.html
<h1>Hello World</h1>

Enfin ouvrez un navigateur et entrez ces URLs :

Vous devriez normalement voir un magnifique Hello World, et la page d'infos de php ! Selection_999_057_.png

Un vrai projet

Je ne vais pas vous laisser avec un simple phpinfo() et un Hello World !!
Nous allons aller au bout des choses, en installant par exemple un petit Drupal.

L'objectif est d'installer Drupal et qu'il soit accessible ( sur votre machine ) depuis l'url http://drupal.local/


Le Vhost

Afin de pouvoir utiliser un nom de domaine locale, ( drupal.local ), il faut auparavant renseigner le fichier hosts, pour le déclarer .

Linux et Mac
sudo sh -c "echo '127.0.0.1   drupal.local' >> /etc/hosts"
Windows

Pour Windows vous devez ouvrir le fichier c:\Windows\System32\Drivers\etc\hosts en administrateur avec NotePad et y ajouter la ligne :

127.0.0.1   drupal.local

Maintenant, nous allons créer notre vhost afin qu'Apache sache quoi faire lorsqu'il reçoit les requêtes avec le nom de domaine drupal.local.

Editer le fichier dockerlamp/docker/apache/vhosts/drupal.conf

<VirtualHost *:80>
        ServerAdmin [email protected]
        ServerName drupal.local

        <Directory />
            AllowOverride All
            Require all granted
        </Directory>

        DocumentRoot "/usr/local/apache2/htdocs/drupal"
        <Directory "/usr/local/apache2/htdocs/drupal">
                Options Indexes FollowSymLinks
                AllowOverride All
                Require all granted
        </Directory>


    <FilesMatch \.php$>
        # 2.4.10+ can proxy to unix socket
        # SetHandler "proxy:unix:/var/run/php5-fpm.sock|fcgi://localhost/"

        # Else we can just use a tcp socket:
        SetHandler "proxy:fcgi://php:9000"
    </FilesMatch>

</VirtualHost>

On a donc déclaré le nom de domaine à Apache, en lui spécifiant de pointer sur le répertoire drupal/

Les sources

Puisque nous avons demandé à Apache de pointer sur le répertoire drupal/, nous allons télécharger les sources de Drupal dans ce répertoire qui lui-même se trouve dans le répertoire dockerlamp/www/

cd www
wget https://ftp.drupal.org/files/projects/https://ftp.drupal.org/files/projects/drupal-8.3.2.tar.gz
tar xzf drupal-8.3.2.tar.gz 
mv drupal-8.3.2 drupal
mkdir -p drupal/sites/default/files/translations && chmod -R 777 sites/default/files/translations
chmod -R 777 drupal/sites/default/files
touch drupal/sites/default/settings.php && chmod 777 drupal/sites/default/settings.php

Enfin, pour que le conteneur Apache prenne en considération notre nouveau vhost, nous allons redémarrer notre stack

docker-compose restart

L'installation

Pour l'installation, il suffit de pointer sur notre nom de domaine local et suivre les instructions

http://drupal.local/

Arrivé à l'installation de la base de données, remplissez le formulaire comme ceci ( Base de données déclarée dans le fichier docker-compose.yml ),

  • Nom de la base de données : project
  • Utilisateur : project
  • Mot de passe : project
  • Et dans options avancés > Hôte : mysql


Selection_999_058_.png

Après cette étape, Drupal s'installe tranquillement sur votre environnement
Selection_999_059_.png
Remplissez les formulaires durant l'installation, vous devriez rapidement arriver sur la page d'accueil de votre Drupal fraîchement installé .

Selection_999_060_.png Selection_999_061_.png

Conclusion

Vous avez à disposition un environnement de développement complet comme démontré avec Drupal.

Pour chaque projet vous devez donc

  1. Créer un domaine local dans votre fichier hosts
  2. Créer la configuration du virtualHost pour apache
  3. Récupérer les sources et les placer dans le répertoire www/
  4. Relancer la stack LAMP via docker-compose

Voici quelques outils / lignes de commandes pour vous aider à travailler, "debugger".

Vous trouverez ces petits scripts dans le répertoire tools/ du dépôt dockerlamp.

Ces scripts ne représentent qu'une base pour vous aider à travailler, je vous laisse maintenant parcourir la documentation de Docker pour comprendre les différentes commandes / configurations utilisées dans ce billet.

N’hésitez pas à poser des questions si besoin.

Créer un dump d'une base de données

dump_sql.sh

docker exec dockerapache_mysql_1 /usr/bin/mysqldump --databases -uroot -psecret $DATABASE > $DATABASE.sql

Restaurer/Importer une base de donnée depuis un dump sql

restore_sql.sh

cat $DUMP | docker exec -i dockerapache_mysql_1 /usr/bin/mysql -uroot -psecret

Afficher les logs Apache en temps réel ( Pour débugger )

logs.sh

docker logs --tail 100 -f dockerapache_httpd_1 

Afficher les ressources utilisées par les conteneurs sur votre machine

stats.sh

docker stats --format "table {{.Container}}\t{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}"

Selection_999_062_.png

Go go go

On clone l'environnement

git clone https://[email protected]/cdesaintleger/dockerlamp.git && cd dockerlamp

On Configure le smtp

cp docker/php/ssmtp.conf.dist docker/php/ssmtp.conf && vim docker/php/ssmtp.conf

On lance la stack

docker-compose up -d

19 oct. 2008

Introduction à PDO

C'est quoi PDO ?

Disponible depuis la version 5.1 de PHP PDO ( PHP DATA OBJECT ) , cette extention permet de développer des applications sans se préoccuper du SGBD qui sera utilisé ... Ainsi , si pour une raison X ou Y vous devez changer de SGBD , seuls quelques paramètres dans votre seront à modifier , vous ne devrez plus réécrire toutes vos requetes .

PDO est ecrit en C , donc plus performant que PEAR DB ou AdoBD , et donne de nombreuses possibilités au développeur , comme les requêtes préparé , transactions ect ...

Premier contact:

Nous allons voir sous forme d'exemples , différentes façons d'utiliser PDO.

Se connecter à une base de données :

//définition des variables de connexion
$user = "toto";
$pwd = "password";

//en fonction du driver
//mysql:
	try{
	   $instancePDO = new PDO("mysql:host=127.0.0.1;dbname=mabase,$user,$pwd");
	} catch (PDOException $e)
	{
		die("Erreur ! : ".$e->getMessage() );
	}
//pgsql:
	try{
	    $instancePDO = new PDO("pgsql:host=127.0.0.1 port=5432 dbname=mabase $user $pwd");
	} catch (PDOException $e)
	{
		die("Erreur ! : ".$e->getMessage() );
	}
//oracle
	try{
	    $instancePDO = new PDO("oci://127.0.0.1:$port/mabase");
	} catch (PDOException $e)
	{
		die("Erreur ! : ".$e->getMessage() );
	}

Les requêtes simples:

A partir de maintenant , je considère être connecté sur une base mysql.

Ici , rien ne change , nous allons utiliser des requêtes mysql standard , la seule différence réside au niveau de la façon dont nous allons envoyer ces requêtes au serveur .

Pour ces tâches , nous allons utiliser les méthodes :

  • exec()
  • query()
exec():

exec sera utilisé pour les requêtes qui ne retournent pas de résultat tel que : INSERT UPDATE DELETE En revanche , exec retourne le nombre de lignes affectées par la requête.

query():

query lui est utilisé pour toutes les requêtes qui renvoient un résultat tel que : SELECT EXPLAIN SHOW DESC cette méthode renvoie une instance de la classe PDOStatement qui utilise le motif itérateur (permet de lire votre objet comme un tableau avec foreach)

Pour résumer :

INSERT => exec()
UPDATE => exec()
DELETE => exec()
SELECT => query()
EXPLAIN => query()
SHOW => query()
DESC => query()

Examples:

SELECTION:

$sql = "SELECT * FROM table LIMIT 10";
$Res = $instance->query($sql);

$Tabres = $Res->fetchAll(PDO::FETCH_ASSOC);
var_dump($Tabres);

l'argument de fetchAll peut être :

PDO::FETCH_ASSOC
PDO::FETCH_BOTH (par defaut)
PDO::FETCH_OBJ

Resultats:

PDO::FETCH_ASSOC

Array
(
    [0]  =>  Array
        (
            [champ1]  =>  valeurA
            [champ2]    =>  valeurB
        )
)

PDO::FETCH_BOTH

Array
(
    [0]  =>  Array
        (
            [champ1]  =>  valeurA
            [0]  => valeurA
            [champ2]    =>  valeurB
            [1]  =>  valeurB
        )
)

PDO::FETCH_OBJ

Array
(
    [0]  =>  stdClass Object
        (
            [champ1]  =>  valeurA
            [champ2]    =>  valeurB
        )
)

SUPPRESSION:

$sql = "DELETE FROM table WHERE etat=1";
$retour = $instance->exec($sql);
print_r($retour);
Resultats:
//Si une erreur est srvenue
FALSE

//Si aucune ligne de table possede son etat = 1
0

//Si plusieurs lignes ont etat = 1 ex:10 lignes
10

Sécurité:

Pour éviter les injections SQL , il faut toujours échapper les données entrantes apres les avoir vérifiées . PDO nous donnes plusieurs façons de procéder ,

La première , est la méthode quote() qui permet d'échapper les chaînes entrantes dans votre base de données , Ensuite , une autre façon de faire , est d'utiliser des requêtes paramétrés , qui n'ont plus besoin d'être échappées , ce qui soulage la conscience du développeur ... et liber son esprit :-) . Nous verrons plus loin comment fonctionne les requêtes paramétrées.

Les transactions:

Les transactions , permettent d'effectuer plusieurs requêtes , Mais de valider celle-ci uniquement si aucune erreurs n'a été détecté , dans le cas contraire , on annule tout ....

Les méthodes qui vont nous servir pour les transactions sont les suivantes :

beginTransaction()
commit()
rollBack()

Je pense que le mieux est d'utiliser un try catch comme ceci :

Exemple:
//La gestion d'erreur doit être en mode exception on y reviendra plus tard
$instancePDO->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$instancePDO->beginTransaction();
try{

    $sql = "INSERT INTO ... ... ";
    $instancePDO->exec($sql);

    $sql = "DELETE FROM ......";
    $instancePDO->exec($sql);

    //si tous se passe bien
    $instancePDO->commit();

} catch (Exception $e) {

    //annule les requetes en cas d'erreur
    $instancePDO->rollBack();

    echo $e->getMessage();
}

Ainsi si une erreur se produit , sur une des requêtes , dans le bloc try , toutes actions effectuées auparavant , sera annulé , et l'on affichera le message d'erreur .

Vous avez certainement remarqué l'appel à la méthode setAttribute en haut du code , cette méthode , permet de changer le gestionnaire d'erreur de PDO . Il y a trois modes possible:

  • mode silencieux , ne pas afficher les erreurs : (defaut)

PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT

  • mode classique , affiche un warning:

PDO::ATTR_ERRMODE,PDO::ERRMODE_WARNING

  • mode exception , lance une exception:

PDO::ATTR_ERRMODE,PDO_ERRMODE_EXCEPTION

Plusieurs méthodes sont aussi disponible pour consulter les informations sur une erreur survenue , par exemple : errorCode()
errorInfo()
disponibles même en mode silencieux !

Les requêtes préparées:

Si vous avez une requête à exécuter de nombreuses fois dans votre script , celle-ci sera à chaque fois , analysé , précompilé optimisé puis exécuté , alors que si vous utilisez une requête préparé , ou seuls les paramètres changent , alors votre requête sera uniquement exécuté , à partir de la seconde fois , ce qui peut être un gain important en terme de performances ...

création d'une requête préparé:

Voilà à quoi ressemble une requête préparée :

$sql = 'INSERT INTO matable (champA, champB) VALUES (?, ?);
ou
$sql = 'INSERT INTO matable (champA, champB) VALUES (:valA, :valB);
puis 
$inststPDO = $instancePDO->prepare($sql);

La méthode prepare() permet de donner votre requête à analyser au SGBD, et renvoie une instance de PDODtatement , tout comme la méthode query() .

Ensuite , il vous faut lier vos données à votre requête. Il existe deux façons simples de le faire . Soit directement , à l'exécution de votre requête , comme ceci :

$valA = "titi";
$valB = "toto";

(array)$param = array(
           ':valA'=>$valA,
           ':valB'=>$valB
         );

$inststPDO->execute($param);

Ensuite , l'on peut aussi utiliser la méthode bindParam() qui lie une variable à un paramètre :

$valA = "titi";
$valB = "toto";

$inststPDO->bindParam(':valA',$valA);
$inststPDO->bindParam(':valB',$valB);

//enregistrement1
$inststPDO->execute();

$valA = "tata";
$valB = "tutu";

//enregistrement2 avec tata tutu
$inststPDO->execute();

Fermeture d'une ressource

En mysql , par exemple , pour libérer les ressources d'une requête , nous utilisions mysql_free_result() Et bien avec PDO , il nous suffit de spécifier notre instance PDOStatement à NULL

$inststPDO = NULL;

Voilà pour cette première approche à PDO , de nombreuse autres subtilités existent , n'hésitez pas à chercher plus loin ...

Cette article à été ecrit avec les références suivantes :