PROJET AUTOBLOG


Nicolargo

source: Nicolargo

⇐ retour index

Varnish, NGinx et PHP-FPM sous Docker

mercredi 8 octobre 2014 à 17:22

Suite au précédent article d'introduction sur Docker (que je vous conseille de lire avant de dévorer cet article), je me suis penché sur le problème suivant: comment créer et maintenir une "stack web" de compétition (Varnish + Nginx + PHP-FPM) basée sur cette technologie de virtualisation par conteneurs.

Quel est l'objectif ?

Nous allons donc, dans ce billet, détailler l'installation et la mise à jour d'une infrastructure, basée sur Docker, qui pourra servir à l'hébergement d'un site personnel, d'un blog ou bien du site d'une PME:

Dans le reste de de l'article, on appellera machine hôte, la machine qui hébergera Docker (lire ce billet pour la procédure d'installation de Docker).

Cette dernière peut être un PC portable ou bien un serveur dédié. C'est un des avantages de la virtualisation par conteneurs. Une fois validé sur une machine , on est sûr que le conteneur fonctionnera de manière identique sur une autre installation de Docker. Ainsi pour la rédaction de ce billet, j'ai utiisé mon PC portable sous Ubuntu 14.04 et un (superbe) VPS mis à disposition par les amis de Web4All sous Debian 7 (merci Aurélien !).

Pour respecter la philosophie de la virtualisation par conteneur, chaque brique sera mise dans un conteneur dédié. On aura ainsi 4 conteneurs sur une machine hôte.

Chaque conteneur communiquera avec les autres selon le schéma ci-dessus.

Les données (data base DB, page statique du site et cache) seront stockées sur la machine hôte.

Création des conteneurs

Il y a deux méthodes pour choisie les images qui seront à la base de nos conteneurs.

La première, la plus noble mais la plus longue à mettre en œuvre, est d'écrire soit même les DockersFiles permettant l'installation et l'exécution des logiciels. On garde ainsi le contrôle de notre infrastructure et la possibilité de configurer finement les applications (notamment au niveau des options de compilation). Dans ce cas, et pour respecter les "best practices" de Docker, il faudra, pour chaque conteneur, repartir d'une image de base de type Debian sur laquelle on viendra installer les briques logicielles de notre LEMP.

La seconde, que j'ai choisi dans ce billet (pas parce-que je suis un gros fainéant mais par manque de temps), est de partir des images disponibles sur la registry officielle de Docker. On gagne ainsi en rapidité de mise en place.  Les composants de notre web stack étant très populaires, on trouve des images supportées et maintenues par la communauté Docker (notamment NGinx). Cependant, on constatera une trop grande diversité dans les systèmes de bases (Ubuntu, CentOS...).

Avant de commencer, nous allons créer un répertoire sur la machine hôte qui hébergera les fichiers utiles à notre infrastructure:

mkdir -p $HOME/data/webstack/conf $HOME/data/webstack/www

Le conteneur NGinx

Pierre angulaire de notre "web stack", le serveur NGinx est très présent sur la "registry" officielle de Docker (plus de 900 images disponibles au moment de l'écriture de ce billet). J'ai choisi d'utiliser l'image officielle proposé par Docker (elle est conçue à partir de cete DockerFile).

On commence par télécharger les images NGinx officielle:

docker pull nginx

Puis on vérifie que les images sont visibles sur notre machine hôte (NGinx version 1.7.5 au moment de l'écriture de cet article):

$ docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
nginx 1.7.5 d2d79aebd368 7 days ago 100.2 MB
nginx latest d2d79aebd368 7 days ago 100.2 MB
nginx 1 d2d79aebd368 7 days ago 100.2 MB
nginx 1.7 d2d79aebd368 7 days ago 100.2 MB

On lance le conteneur:

$ docker run --name webstack_nginx_1 -v $HOME/data/webstack/www:/usr/share/nginx/html:ro -p 8080:80 -d nginx
f7baa81cebdc8799947327e6470a74ae73fe73bb0eb644ecfb2951847c40154b

Notre conteneur a le doux nom de webstack_nginx_1, il redirige le port TCP/8080 de notre hôte vers le port TCP/80 du conteneur (port par défaut de NGinx) et il assigne, en lecture seule, le volume /usr/share/nginx/html au répertoire hôte $HOME/data/www (à adapter à votre configuration). Pour résumer, toutes les pages HTML disponible dans le répertoire /home/nicolargo/data/www, seront accessible via le port HTTP/8080 de votre machine locale.

On ajoute une page HTML statique de test et une autre pour le test PHP que l'on utilisera dans le chapitre suivant:

echo "My first page" > $HOME/data/webstack/www/index.html
echo "<?php phpinfo(); ?>" > $HOME/data/webstack/www/phpinfo.php

Puis on valide que le serveur NGinx est bien opérationnel:

$ curl http://localhost:8080
My first page

Ou directement depuis votre navigateur Web préféré:

Sélection_245L'image NGinx utilisée redirige les fichiers de log (access.log et error.log vert la sortie standard). Il est donc possible de visualiser les accès à son serveur Web en "attachant" le terminal de notre hôte à la sortie standard du conteneur:

$ docker attach --sig-proxy=false webstack_nginx_1
2014/10/08 13:47:59 [error] 9#0: *1 open() "/usr/share/nginx/html/inconnu.html" failed (2: No such file or directory), client: 172.17.0.103, server: localhost, request: "GET /inconnu.html HTTP/1.1", host: "localhost"
172.17.0.103 - - [08/Oct/2014:13:47:59 +0000] "GET /inconnu.html HTTP/1.1" 404 168 "-" "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:32.0) Gecko/20100101 Firefox/32.0" "172.17.42.1"

Note: bien penser à mettre l'option --sig-proxy=true sous peine de stopper le conteneur lors de l'appuie sur ^C. Je vous conseille  de créer un alias dans votre shell.

Le conteneur PHP-FPM

Pour le moteur PHP, il n'existe pas d'image officielle. Mon choix s'est donc porté vers l'utilisation du repository de Jprjr basée sur une installation de PHP-FPM sous ArchLinux avec pas mal d'extensions par défaut (voir la liste dans le DockerFile).

On télécharge la dernière version des images avec:

docker pull jprjr/php-fpm

On obtient bien:

$ docker images | grep jprjr
jprjr/php-fpm          latest                 d40698b35f83        6 weeks ago         347.8 MB

On lance le conteneur:

docker run --name webstack_php_01 -p 9000:9000 -d jprjr/php-fpm

Le conteneur est configuré par défaut pour écouter sur le port TCP/9000.

Très bien, on doit donc avoir le serveur NGinx et le moteur PHP-FPM qui sont lancés dans deux conteneurs différents. On va vérifier cela avec la commande:

$ docker ps
CONTAINER ID        IMAGE                  COMMAND                CREATED             STATUS              PORTS                           NAMES
e9dd04e7ceec        jprjr/php-fpm:latest   "php-fpm -F"           13 seconds ago      Up 12 seconds       0.0.0.0:9000->9000/tcp          webstack_php_01
f7baa81cebdc        nginx:1                "nginx -g 'daemon of   46 minutes ago      Up 46 minutes       443/tcp, 0.0.0.0:8080->80/tcp   webstack_nginx_01

Faire communiquer les deux conteneurs (NGinx & PHP-FPM)

Tout cela est très beau mais les deux conteneurs ne se connaissent pas. Il faut, pour cela, configurer le serveur NGinx pour utiliser le moteur PHP et donc que le conteneur NGinx connaisse l'adresse IP du conteneur PHP-FPM.

Heureusement, Docker propose l'option --link permettant de répondre à ce besoin. Cette option, à utiliser au moment du lancement du conteneur, va créer dynamiquement des entrées dans le fichier host et dans des variables d'environnement du conteneur, permettant ainsi à ce dernier de connaître comment joindre ses congénères.

Par exemple:

$ docker run -it -v --link webstack_php_01:webstack_php nginx /bin/bash
root@2ce3cdb8a767:/# cat /etc/hosts | grep webstack
172.17.0.5	webstack-php
root@d9ff8a80f12a:/# env | grep WEBSTACK
WEBSTACK_PHP_PORT=tcp://172.17.0.5:9000
WEBSTACK_PHP_PORT_9000_TCP=tcp://172.17.0.5:9000
WEBSTACK_PHP_PORT_9000_TCP_ADDR=172.17.0.5
WEBSTACK_PHP_PORT_9000_TCP_PORT=9000
WEBSTACK_PHP_NAME=/webstack_nginx/webstack_php
WEBSTACK_PHP_PORT_9000_TCP_PROTO=tcp
root@2ce3cdb8a767:/# exit
exit
$ docker rm `docker ps -lq`

Maintenant que l'on sait comment faire communiquer les deux conteneurs, il suffit de configurer NGinx pour rediriger les fichier .PHP vers le moteur PHP-FPM du second conteneur.

Pour cela, plusieurs solutions sont là encore possibles. La plus simple est de surcharger la configuration NGinx par défaut(nginx.conf). Sur notre hôte, nous allons créer le fichier de configuration en question:

$ docker webstack_nginx_01/etc/nginx/nginx.conf $HOME/data/webstack/conf/nginx.conf
$ vi $HOME/data/webstack/conf/nginx.conf
daemon off;
user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    #gzip  on;
    server {
        listen       80;
        server_name  localhost;
        location / {
            root   /usr/share/nginx/html;
            index  index.html index.htm;
        }
        #error_page  404              /404.html;
        # redirect server error pages to the static page /50x.html
        #
        #error_page   500 502 503 504  /50x.html;
        #location = /50x.html {
        #    root   /usr/share/nginx/html;
        #}
        # Pass PHP scripts to PHP-FPM
        location ~* \.php$ {
            fastcgi_index   index.php;
            fastcgi_pass    webstack_php:9000;
            #fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
            include         fastcgi_params;
            fastcgi_param   SCRIPT_FILENAME    /srv/http$fastcgi_script_name;
            fastcgi_param   SCRIPT_NAME        $fastcgi_script_name;
        }
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        location ~ /\.ht {
            deny  all;
        }
    }
}

Note:

Revenons à notre cas. Nous souhaitons que le conteneur NGinx (nommé webstack_nginx_01) connaisse le conteneur PHP (webstack_php_01).

On commence donc par supprimer les deux conteneurs existant:

docker stop webstack_php_01 && docker rm webstack_php_01
docker stop webstack_nginx_01 && docker rm webstack_nginx_01

puis de les recréer avec l'option --link (pour le conteneur NGinx qui va initier la connexion TCP vers le moteur PHP-FPM), la nouvelle configuration de NGinx et les volumes pointant sur notre page HTML et PHP:

docker run --name webstack_php_01 -v $HOME/data/webstack/www:/srv/http:ro -p 9000:9000 -d jprjr/php-fpm
docker run --name webstack_nginx_01 -v $HOME/data/webstack/www:/usr/share/nginx/html:ro -v $HOME/data/webstack/conf/nginx.conf:/nginx.conf:ro -p 8080:80 --link webstack_php_01:webstack_php -d nginx nginx -c /nginx.conf

On teste:

$ curl http://localhost:8080/index.html
My first page
$ curl http://localhost:8080/phpinfo.php
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<style type="text/css">
...

Sélection_246

Bingo !

A ce stade, nous avons donc deux conteneurs communiquant entre eux pour nous offrir un serveur Web compatible avec le langage PHP.

Le conteneur Varnish en frontal

Si vous suivez ce blog, vous savez tout le bien que je pense de l'utilisation du cache Varnish pour absorber la montée en charge des sites Web (sinon, vous pouvez effectuer une session de rattrapage en lisant ces articles). Nous allons donc créer un nouveau conteneur qui exposera le port TCP/80 et qui mettra en cache les pages construites par le serveur NGinx (qui est en écoute sur le port TCP/8080 si vous avez bien suivi...).

Nous allons utiliser l'image Varnish mise à disposition et maintenue par le contributeur jacksoncage.

On commence par la récupérer:

docker pull jacksoncage/varnish

Puis on vérifie qu'elle est bien disponible sur notre hôte:

$ docker images | grep jacksoncage
jacksoncage/varnish    latest                 46f0ea7021c1        8 months ago        472.2 MB

Comme il est indiqué dans la documentation de l'image, il faut écrire le fichier de configuration par défaut de Varnish (le fichier default.vcl):

$ vi $HOME/data/webstack/conf/default.vcl
backend default {
    .host = "webstack_nginx";
    .port = "80";
}

On demande donc à Varnish d'aller directement communiquer avec le conteneur NGinx via l'adresse IP webstack-nginx (rappelez-vous que le hostname est créé dynamiquement par Docker au démarrage du conteneur avec l'option --link) et sur le port TCP 80 (attention, ce n'est pas le port TCP/8080 exposé par Docker sur notre hôte mais celui vraiment utilisé par NGinx dans le conteneur).

A noter que cette configuration est à compléter, notamment si vous voulez héberger un blog sous WordPress (des exemples de fichiers de configuration sont disponibles ici). Attentinon, ces exemples sont pour Varnish 4.0, donc à adapter si la version du conteneur jacksoncage/varnish n'est pas en ligne.

Puis lancer le conteneur avec les options suivantes:

docker run --name webstack_varnish_01 -v $HOME/data/webstack/conf/default.vcl:/etc/varnish/default.vcl:ro -p 80:80 --link webstack_nginx_01:webstack_nginx -d jacksoncage/varnish

On redirige le port 80 du conteneur vers le port 80 de votre hôte (il ne doit bien sûr pas y avoir de serveur Web déjà en écoute sur ce port) puis on fait le lien entre le conteneur Varnish et le conteneur NGinx.

On vérifie que nos trois conteneurs sont lancés:

$ docker ps
CONTAINER ID        IMAGE                        COMMAND                CREATED             STATUS              PORTS                           NAMES
a1b853f1a38e        jacksoncage/varnish:latest   "/start"               4 seconds ago       Up 3 seconds        0.0.0.0:80->80/tcp              webstack_varnish_01
c2bd3138864a        nginx:1                      "nginx -c /nginx.con   54 minutes ago      Up 54 minutes       443/tcp, 0.0.0.0:8080->80/tcp   webstack_nginx_01,webstack_varnish_01/webstack-nginx
7cc072bb0df2        jprjr/php-fpm:latest         "php-fpm -F"           54 minutes ago      Up 54 minutes       0.0.0.0:9000->9000/tcp          ...

Et on teste notre serveur de cache Varnish:

$ curl -I http://localhost/index.html
HTTP/1.1 200 OK
Server: nginx/1.7.5
Content-Type: text/html
Last-Modified: Sat, 04 Oct 2014 16:30:45 GMT
ETag: "543020b5-15"
Content-Length: 21
Accept-Ranges: bytes
Date: Sun, 05 Oct 2014 15:26:47 GMT
X-Varnish: 320901887 320901884
Age: 46
Via: 1.1 varnish
Connection: keep-aliv

Le "Via: 1.1 varnish" confirme que notre infra commence à avoir de la gueule :).

En aparté, on peut d'ailleurs juger de la puissance de Varnish en lançant un simple bench tout d'abord directement sur le serveur NGinx (donc en baille-passant Varnish):

$ ab -t 30 -c 5 http://localhost:8080/
Complete requests:      7107
Requests per second:    232.24 [#/sec] (mean)
Time per request:       21.529 [ms] (mean)
Time per request:       4.306 [ms] (mean, across all concurrent requests)

Puis en faisant le même test en passant par Varnish:

$ ab -t 30 -c 5 http://localhost/
Complete requests:      50000
Requests per second:    3694.60 [#/sec] (mean)
Time per request:       1.353 [ms] (mean)
Time per request:       0.271 [ms] (mean, across all concurrent requests)

Les chiffres parlent d'eux même...

Gestion des conteneurs

Un simple pull sur les images préalablement téléchargé s'occupera de leurs mises à jour:

docker pull nginx
docker pull jprjr/php-fpm
docker pull jacksoncage/varnish

Comme l'on reconstruit notre infrastructure à partir de ces images, il suffira ensuite d'arrêter et de relancer les conteneurs:

docker stop webstack_varnish_01 && docker rm webstack_varnish_01
docker stop webstack_nginx_01 && docker rm webstack_nginx_01
docker stop webstack_php_01 && docker rm webstack_php_01
docker run --name webstack_php_01 -v $HOME/data/webstack/www:/srv/http:ro -p 9000:9000 -d jprjr/php-fpm
docker run --name webstack_nginx_01 -v $HOME/data/webstack/www:/usr/share/nginx/html:ro -v $HOME/data/webstack/conf/nginx.conf:/nginx.conf:ro -p 8080:80 --link webstack_php_01:webstack_php -d nginx nginx -c /nginx.conf
docker run --name webstack_varnish_01 -v $HOME/data/webstack/conf/default.vcl:/etc/varnish/default.vcl:ro -p 80:80 --link webstack_nginx_01:webstack_nginx -d jacksoncage/varnish

Note: Il est également possible de créer des images maison (avec commit + tag) et de les relancer. Cette phase peut être utile dans le cadre d'un déploiement d'une infrastructure à une autre.

Orchestration de l'infrastructure

On vient de voir que le lancement d'une infrastructure basée sur Docker peut rapidement devenir compliqué. Les conteneurs doivent être lancés dans un certain ordre, avec des options spécifiques. Il est toujours possible de scripter les commandes du chapitre précédent ou plus simplement d'utiliser un outil d'orchestration.

Vagrant vous vient en tête ? Pourtant, ce n'est pas la solution que nous allons utiliser dans ce billet.

Nous allons nous tourner vers Fig. C'est un logiciel en ligne de commande permettant de d'écrire son infrastructure Docker à partir d'un fichier de configuration texte au format YAML.

L'installation de Fig se fait via la commande:

sudo apt-get install fig

Note: pour d'autre méthode d'installation, consultez la documentation sur le site officiel.

Le fichier de configuration Fig correspondant à notre infrastructure est le suivant (à éditer dans le fichier $HOME/data/webstack/fig.yml):

# Webstack PHP
php:
    image: jprjr/php-fpm
    volumes:
    - www:/srv/http
    ports:
    - 9000:9000
# Webstack NGINX
nginx:
    image: nginx
    links:
    - php:webstack_php
    volumes:
    - www:/usr/share/nginx/html
    - conf/nginx.conf:/nginx.conf
    ports:
    - 8080:80
    command: nginx -c /nginx.conf
# Webstack VARNISH
varnish:
    image: jacksoncage/varnish
    links:
    - nginx:webstack_nginx
    volumes:
    - conf/default.vcl:/etc/varnish/default.vcl
    ports:
    - 80:80

On lance ensuite notre infrastructure en une seule et unique commande:

$ fig up -d
Creating webstack_php_1...
Creating webstack_nginx_1...
Creating webstack_varnish_1...

Tout comme avec la ligne de commande Docker, on peut voir les conteneurs en cours d'exécution:

$ fig ps
       Name                Command          State           Ports
-------------------------------------------------------------------------
webstack_php_1                              Up      9000->9000/tcp
webstack_nginx_1     nginx -c /nginx.conf   Up      443/tcp, 8080->80/tcp
webstack_varnish_1   /start                 Up      80->80/tcp

Les arrêter (sans les supprimer):

$ fig stop
Stopping webstack_varnish_1...
Stopping webstack_nginx_1...
Stopping webstack_php_1...

Les relancer:

$ fig start
Starting webstack_php_1...
Starting webstack_nginx_1...
Starting webstack_varnish_1...

Les supprimer:

fig stop && fig rm

Je vous laisse découvrir les autres commandes de Fig sur le site officiel de la documentation.

Conclusion

Nous venons de créer les bases d'une infrastructure Web performante, facilement maintenable et évolutive. Il est ainsi facile d'y ajouter d'autres services comme une base de donnée (type MariaDB), un serveur sFTP (pour la mise à jour des pages Web) ou bien encore un outil d'analyse des logs (Varnish et NGinx).

Et de votre coté, avez-vous mis en place une infrastructure Docker pour votre serveur Web ? Si oui comment ?

Partagez votre expérience avec nous !

Cet article Varnish, NGinx et PHP-FPM sous Docker est apparu en premier sur Le blog de NicoLargo.