Varnish, NGinx et PHP-FPM sous Docker
Date: 8/10/2014 | Catégories: Blog,Open-source,Systeme,vitualisation | Tags: docker,docker-compose,fig,lemp,nginx,php-fpm,varnish,web stack
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:
- la base de donnée sera portée par l'implémentation libre de MySQL, c'est à dire MariaDB (nous aborderons cette partie dans un deuxième billet)
- pour le moteur PHP, j'utilise PHP-FPM
- pour le serveur Web, je ne jure que par Nginx (léger, bien documenté)
- afin pour tenir la charge (notamment si vous utilisé un moteur de blog de type WordPress, j'utilise le système de cache Varnish)
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é:
L'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:
- "fastcgi_pass webstack_php:9000;" qui utilise donc le hostname présent dans le fichier host
- "fastcgi_param SCRIPT_FILENAME /srv/http$fastcgi_script_name;" qui va permettre à PHP-FPM d'aller chercher les pages PHP dans le même volume que le serveur Nginx mais assigné au répertoire /srv/http du conteneur (c'est la configuration par défaut de notre conteneur PHP-FPM)
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"> ...
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 docker-compose (anciennement 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 pip install docker-compose
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/docker-compose.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:
$ docker-compose 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:
$ docker-compose 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):
$ docker-compose stop Stopping webstack_varnish_1... Stopping webstack_nginx_1... Stopping webstack_php_1...
Les relancer:
$ docker-compose start Starting webstack_php_1... Starting webstack_nginx_1... Starting webstack_varnish_1...
Les supprimer:
docker-compose stop && docker-compose rm
Je vous laisse découvrir les autres commandes de docker-compose 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 !