Pour un projet en cours de finalisation, j’ai utilisé le CMS Grav et j’ai décidé tant pour mon développement en local que pour l’environnement de production de déployer cela sous la forme de container docker et d’utiliser Traefik comme reverse-proxy et m’appuyer notamment sur son support natif et dynamique des containers docker.
Premiers pas…
J’avais initialement publié une image nsteinmetz/grav basée sur l’image officielle PHP:apache mais elle ne me convenait pas totalement :
- Utilisation d’apache2 alors que j’ai basculé depuis longtemps sur nginx
- L’image
PHP:apacheest basée sur Debian (taille plus importante qu’une image basée sur Alpine) - Pas d’utilisation des volumes (je n’ai pas pris le temps de les déclarer)
Pour mémoire et à toutes fins utiles, le Dockerfile - nsteinmetz/docker-grav :
FROM php:7-apache
ADD https://github.com/getgrav/grav/releases/download/1.1.8/grav-admin-v1.1.8.zip /tmp/grav-admin-v1.1.8.zip
RUN apt update && \
apt upgrade -y && \
apt install -y \
unzip \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng12-dev \
pkg-config && \
unzip /tmp/grav-admin-v1.1.8.zip -d /tmp/ && \
mv /tmp/grav-admin/* /var/www/html/ && \
mv /tmp/grav-admin/.htaccess /var/www/html/ && \
chown www-data:www-data -R /var/www/html && \
docker-php-ext-install -j$(nproc) mcrypt && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
docker-php-ext-install -j$(nproc) gd &&\
docker-php-ext-install -j$(nproc) zip &&\
a2enmod rewrite && \
rm -rf /var/lib/apt/lists/*
COPY php.conf /etc/apache2/conf-enabled/
COPY server-signature.conf /etc/apache2/conf-enabled/
Avec php.conf :
php_admin_flag display_errors off
php_admin_flag expose_php off
php_admin_value post_max_size "40M"
php_admin_value upload_max_filesize "40M"
et server-signature.conf :
ServerSignature Off
ServerTokens Prod
Améliorations
Les améliorations apportées :
- Passage à
docker-composepour avoir plus de flexibilité au moment de démarrer les containers en fonction des projets utilisant Grav, - Passage à des images basées sur alpine (plus légères)
- Passage à une architecture PHP-FPM + Nginx
Cela donne :
├── app
│ ├── Dockerfile
│ ├── security.conf
│ └── uploads.conf
└── web
│ ├── Dockerfile
│ └── php-fpm.conf
├── docker-compose.yml
Petites précisions à ce niveau :
appcorrespond au container php-fpmsecurity.confcontient des options de sécurité PHP (ne pas exposer la version de PHP, ne pas afficher les erreurs, etc)uploads.confcontient des options liés aux uploads PHP (taille max de fichiers, etc)
webest le container nginxphp-fpm.confest la configuration de mon virtualhost nginx
Revue du container “app” (php-fpm)
On a app/Dockerfile :
FROM php:7-fpm-alpine
ADD https://github.com/getgrav/grav/releases/download/1.1.8/grav-admin-v1.1.8.zip /tmp/grav-admin-v1.1.8.zip
RUN apk update &&\
apk upgrade &&\
unzip /tmp/grav-admin-v1.1.8.zip -d /tmp/ && \
mv /tmp/grav-admin/* /var/www/html/ && \
mv /tmp/grav-admin/.htaccess /var/www/html/ && \
chown www-data:www-data -R /var/www/html &&\
apk add libjpeg libjpeg-turbo libjpeg-turbo-dev libpng libpng-dev freetype freetype-dev &&\
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
docker-php-ext-install mcrypt zip gd &&\
rm -rf /var/cache/apk/* &&\
rm -rf /tmp/grav-admin-v1.1.8.zip &&\
cd /var/www/html && ./bin/gpm update --no-interaction
COPY uploads.conf /usr/local/etc/php-fpm.d/uploads.conf
COPY security.conf /usr/local/etc/php-fpm.d/security.conf
VOLUME ["/var/www/html", "/var/www/html/assets", "/var/www/html/backup", "/var/www/html/cache", "/var/www/html/images", "/var/www/html/logs", "/var/www/html/tmp"]
A noter, même s’il n’y a rien d’exceptionnel :
- Initialisation du container en récupérerant la version de Grav, gestion des dépendances et des permissions
- La dernière ligne de
RUNva mettre à jour les plugins inclus nativement dans Grav - Les volumes déclarés permettent de sortir du container toutes les zones non stateless : logs, cache, contenus, etc sur la base de la documentation officielle de Grav ; j’ai ajouté néanmoins les répertoire
backupsettmp.
Ensuite, app/security.conf:
[www]
php_admin_flag[display_errors] = off
php_admin_flag[expose_php] = off
et app/uploads.conf :
[www]
php_admin_value[post_max_size] = "40M"
php_admin_value[upload_max_filesize] = "40M"
J’ai fait le choix de surcharger l’instance php-fpm par défaut (ie: www) et j’utilise php_admin_* afin d’interdire toute surcharge de ces variables par l’application.
Revue du container “web” (nginx)
web/Dockerfile :
FROM nginx:stable-alpine
RUN rm /etc/nginx/conf.d/default.conf
ADD php-fpm.conf /etc/nginx/conf.d/php-fpm.conf
Je supprime le virtualhost fourni par défaut et je fournis le mien en lieu et place.
web/php-fpm.conf :
server {
listen 80;
server_name _;
charset utf-8;
root /var/www/html/;
index index.html index.php;
# Uploads to 100M
client_max_body_size 100m;
location / {
try_files $uri $uri/ /index.php?_url=$uri;
}
## Begin - Security
# don't send the nginx version number in error pages and Server header
server_tokens off;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header X-Frame-Options SAMEORIGIN;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: ; style-src 'self' 'unsafe-inline'; font-src 'self'; child-src; object-src 'none'";
# deny all direct access for these folders
location ~* /(.git|cache|bin|logs|backup|tests)/.*$ { return 403; }
# deny running scripts inside core system folders
location ~* /(system|vendor)/.*\.(txt|xml|md|html|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; }
# deny running scripts inside user folder
location ~* /user/.*\.(txt|md|yaml|php|pl|py|cgi|twig|sh|bat)$ { return 403; }
# deny access to specific files in the root folder
location ~ /(LICENSE.txt|composer.lock|composer.json|nginx.conf|web.config|htaccess.txt|\.htaccess) { return 403; }
## End - Security
## Begin - PHP
location ~ \.php$ {
fastcgi_pass app:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
}
## End - PHP
location ~* ^.+\.(ico|js|gif|jpg|jpeg|png|bmp)$ {
expires 30d;
}
}
A noter :
- Le fichier est inspiré de la configuration officielle nginx donnée pour Grav
- Ajout de directives de sécurité (
X-Content-Type-Options,X-XSS-Protection,X-Frame-OptionsetContent-Security-Policy) - Interdiction d’accès à des répertoires
- Déclaration de php-fpm où le point d’attention est juste d’indiquer
app:9000, carappest le nom de mon service. - Mise en cache des images
Et un docker-compose.yml pour enrober le tout
version: '2'
services:
web:
build: ./web/
depends_on:
- app
volumes_from:
- app
ports:
- "80:80"
app:
build: ./app/
volumes:
- ./user:/var/www/html/user
- ./assets:/var/www/html/assets
- ./backup:/var/www/html/backup
- ./cache:/var/www/html/cache
- ./images:/var/www/html/images
- ./logs:/var/www/html/logs
- ./tmp:/var/www/html/tmp
Pour builder puis lancer vos containers en mode daemon :
docker-compse up -d --build
Traefik en reverse-proxy
Traefik est un reverse-proxy moderne et il a le bon goût de s’interfacer notamment avec l’API de Docker. On peut alors déclarer dynamiquement nos containers à traefik et celui-ci les prend en compte dynamiquement.
Démarrer Traefik
Pour mon poste en local, j’ai décidé de lancer Traefik de la façon suivante :
docker run -d -v /dev/null:/traefik.toml -v /var/run/docker.sock:/var/run/docker.sock -p 80:80 -p 443:443 -p 8080:8080 --name traefik traefik:camembert --web --docker --docker.domain=docker.localhost --logLevel=DEBUG
Pour les détails :
-v /dev/null:/traefik.tomlpermet de démarrer Traefik sans fichier de configuration-v /var/run/docker.sock:/var/run/docker.sockpermert au container Traefik de communiquer avec Docker et son API- J’expose les ports 80, 443 et 8080. Le dernier permettra d’accéder au dashboard fourni nativement par Traefik
--web: lance l’interface web (dashboard)--docker --docker.domain=docker.localhost: active le support de docker et fourni un domaine par défaut à tous les containers docker.--logLevel=DEBUG: permet d’avoir des logs verbeux si problème viadocker logs -f traefik
Rendre son application “traefik-aware”
Il “suffit” d’indiquer des labels à vos services :
version: '2'
services:
web:
build: ./web/
depends_on:
- app
volumes_from:
- app
labels:
- "traefik.backend=grav-project"
- "traefik.frontend.rule=Host:project.grav"
- "traefik.port=80"
- "traefik.protocol=http"
- "traefik.frontend.entryPoints=http"
- "traefik.docker.network=nom_du_reseau"
app:
build: ./app/
volumes:
- ./user:/var/www/html/user
- ./assets:/var/www/html/assets
- ./backup:/var/www/html/backup
- ./cache:/var/www/html/cache
- ./images:/var/www/html/images
- ./logs:/var/www/html/logs
- ./tmp:/var/www/html/tmp
labels:
- "traefik.enable=false"
Explications pour le service web :
- je lui indique un nom de backend (peu importe le nom) qui sert juste à Traefik pour “nommer” votre container
- l’url (de frontend) sur laquelle Traefik devra réagir pour vous interfacer avec votre container (ici en tapant http://project.grav/ dans mon navigateur, je dois arriver sur mon container)
- le protocol et le port indiquent que je me connecte sur le port 80 de mon container avec le protocole http
- l’
entryPointindique que je vais me connecter à Traefik sur le port 80 pour me connecter ensuite à mon conteneur. - le nom du réseau que vous a créé docker lors de l’initialisation du projet. Pour le récupérer, faire un
docker network ls.
En gros on a :
Vous <=> Votre navigateur <=> Traefik Frontend (
Host:project.grav(frontend.rule) + protocolehttp(EntryPoint)) <=> Traefik Backend (grav-project+ port80+ protocolehttp) <=> ContainerWeb
Explications pour le service app :
- Je ne souhaite pas exposer ce service dans Traefik, je désactive alors ce service (par défault, il y a une auto-découverte de tous les services).
Pour finir
Il ne me reste plus qu’à :
- Faire une entrée DNS ou éditer
/etc/hostspourproject.grav - Ouvrir votre navigateur sur
http://project.grav/
Sauf que cela ne marche pas à ce stade (si vous utilisez traefik dans un container docker). En effet, lors du docker-compose up -d, docker a créé un réseau pour votre application. Or par défaut, Traefik n’y a pas accès. Il vous faut faire:
docker network connect <nom_du_reseau> <container-traefik>
Et ce coup-ci, votre site propulsé par Grav s’affiche.
Une fois prochaine; je vous parlerais d’Ansible, Docker et Traefik pour déployer vos projets aisément.