docker

WordPress et Traefik – Mise en place d’une stack docker-compose

Objectif : mettre en place Traefik en frontend, l’application WordPress, Nginx en tant que serveur web, une base de données SQL grâce à MariaDB et un serveur de cache objets via Redis.

Mise à jour le 01/02/2021.

Comme les autres articles, j’utilise le dossier /srv/docker pour y placer les fichiers de la stack docker-compose. Un dossier « conf » sera créé et utiliser pour les fichiers de configuration des différents applicatifs.

Logiciels exploités :

  • Debian 10.7
  • Docker CE = 20.10.x
  • Docker-compose = 1.28.x
  • Traefik = 2.4 « livarot »
  • Nginx = 1.19 (stable)
  • MariaDB = 10.5 (stable)
  • WordPress =  5.6
  • PHP = 7.4-fpm
  • Redis = 6

Prépration de la machine et des fichiers de configuration docker

Pré-requis : créer les dossiers et placer les droits nécessaires pour les configurations des différents services :

mkdir -p /srv/docker/conf/traefik/traefikdynamic
mkdir /srv/docker/conf/nginx-wp
mkdir /srv/docker/logs
touch /srv/docker/logs/traefik.log
touch /srv/docker/conf/acme.json
chmod 0600 /srv/docker/conf/acme.json

Ci-dessous le fichier « traefik.yml » (/srv/docker/conf/traefik.yml) :

global:
  sendAnonymousUsage: false
  checkNewVersion: false

api:
  #insecure: true
  dashboard: true
  #debug: true

log:
  filePath: "/etc/traefik/applog.log"
  level: INFO

providers:
  docker:
    endpoint: unix:///var/run/docker.sock
    exposedByDefault: false
    watch: true
    swarmMode: false

  file:
    directory: /traefikdynamic
    watch: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

certificatesResolvers:
  letsencrypt:
    acme:
      email: [email protected]
#      caServer: https://acme-staging-v02.api.letsencrypt.org/directory
      caServer: https://acme-v02.api.letsencrypt.org/directory
      storage: acme.json
      keyType: EC256
      httpChallenge:
        entryPoint: web

Passons au fichier traefik_dynamic.yml (/srv/docker/conf/traefikdynamic/traefik_dynamic.yml) :

tls:
  options:
    default:
      minVersion: VersionTLS12
      sniStrict: true
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
      curvePreferences:
        - CurveP521
        - CurveP384

http:
  middlewares:
    compression:
      compress:
        excludedContentTypes:
          - text/event-stream

    security:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        #accessControlAllowOriginList = "*"
        accessControlMaxAge: 100
        addVaryHeader: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        frameDeny: true
        sslRedirect: true
        sslForceHost: true
        stsPreload: true
        #ContentSecurityPolicy = "default-src 'self' 'unsafe-inline'"
        customFrameOptionsValue: SAMEORIGIN
        referrerPolicy: same-origin
        featurePolicy: vibrate 'self'
        stsSeconds: 315360000

    authentification:
      basicAuth:
        users: # admon / totolasticot
        - admon:$2y$10$4IgUsvlCwENsF18B9t7AoegQ7eWZVfWxPFRhNfBWz7F00h1X2oZFe

Fichier docker-compose.yml comportant tous les services – quelques explications seront juste en dessous du code.

---
version: '3.6'
services:
  traefik:
    container_name: traefik
    image: traefik:${TRAEFIKVERSION}
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
      - 8080:8080 # used to have the traefik dashboard
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./conf/traefik.yml:/etc/traefik/traefik.yml:ro
      - ./conf/traefikdynamic:/traefikdynamic:ro
      - ./logs/traefik.log:/etc/traefik/applog.log
      - ./conf/acme.json:/acme.json

  sqlwp:
    container_name: sqlwp
    image: mariadb:${MARIADBVERSION}
    restart: unless-stopped
    volumes:
      - ./conf/custom-mysql.cnf:/etc/mysql/conf.d/custom-mysql.cnf
      - datasqlwp:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQLROOT}
      MYSQL_USER: ${MYSQLUSER}
      MYSQL_PASSWORD: ${MYSQLPASSWORD}
      MYSQL_DATABASE: ${MYSQLDB}
    labels:
      traefik.enable: false
 
  nginxwp:
    container_name: nginxforwp
    image: nginx:${NGINXVERSION}
    restart: unless-stopped
    volumes:
      - datawp:/var/www/html
      - datanginxlogs:/var/log/nginx/
      - ./conf/nginx-wp:/etc/nginx/
    links:
      - wp
    labels:
      traefik.enable: true
      traefik.http.routers.nginxwp-https.entrypoints: websecure
      traefik.http.routers.nginxwp-https.rule: Host(`wp.czs.local`)
      traefik.http.routers.nginxwp-https.middlewares: [email protected], [email protected]
      traefik.http.routers.nginxwp-https.tls: true
      traefik.http.routers.nginxwp-https.tls.certresolver: letsencrypt

  wp:
    container_name: wpapp
    image: wordpress:${WPVERSION}
    restart: unless-stopped
    volumes:
      - ./conf/php.ini:/usr/local/etc/php/php.ini
      - datawp:/var/www/html
    depends_on:
      - sqlwp
      - redis
    environment:
      WORDPRESS_DB_HOST: sqlwp
      WORDPRESS_DB_USER: ${MYSQLUSER}
      WORDPRESS_DB_PASSWORD: ${MYSQLPASSWORD}
      WORDPRESS_DB_NAME: ${MYSQLDB}
      WORDPRESS_TABLE_PREFIX: ${MYSQLTABLEPREFIX}
    labels:
      traefik.enable: false

  redis:
    container_name: redis
    image: redis:${REDISVERSION}
    restart: unless-stopped
    volumes:
      - dataredis:/data
    command: redis-server --maxmemory 1024mb --maxmemory-policy allkeys-lru --requirepass changemeWithALongPassword --appendonly yes --bind redis
    labels:
      traefik.enable: false

volumes:
  datasqlwp:
  datanginxlogs:
  datawp:
  dataredis:

Traefik est le reverse-proxy frontal. Le serveur nginx est présent pour afficher les ressources de WordPress : en effet, le conteneur WordPress n’a pas de serveur web, uniquement le socket PHP-FPM et les fichiers statiques de l’application. Traefik n’est pas (encore) capable de servir de serveur web, il est donc nécessaire d’utiliser nginx pour se faire. La base de données et Redis sont plutôt orienté « backend » pour WordPress.

Concernant le conteneur Redis, n’oubliez pas de changer le --maxmemory en fonction de la RAM disponible sur votre machine. Par exemple, si vous avez un serveur disposant de 4 Go de RAM, alors préférez 2 Go max pour Redis.

Fichier .env (contient les variables avec les mots de passe pour le SQL et les numéros de version)

MYSQLUSER=wpsqluser
MYSQLROOT=motdepasseadmin
MYSQLPASSWORD=sqlpassword
MYSQLDB=wpdb
MYSQLTABLEPREFIX=lkEZKOl
TRAEFIKVERSION=livarot
MARIADBVERSION=focal
NGINXVERSION=stable
WPVERSION=5-php7.4-fpm
REDISVERSION=6

Fichier « wp.conf » (/srv/docker/conf/nginx-wp/wp.conf) / utilisé par Nginx pour accéder / afficher WordPress.

server {
    listen 80;
    server_name wp.czs.local;

    root /var/www/html;
    index index.php;

    access_log /var/log/nginx/wp-access.log;
    error_log /var/log/nginx/wp-error.log;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    # Remove X-Powered-By, which is an information leak
    fastcgi_hide_header X-Powered-By;

    location = /robots.txt {
        log_not_found off;
        access_log off;
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass wp:9000;
        include /etc/nginx/fastcgi.conf;
    }

    location ~* .(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 365d;
    }
}

Fichier « nginx.conf » (/srv/docker/conf/nginx-wp/nginx.conf) / configuration de base pour Nginx, sans fioritures.

worker_processes auto; # use "grep processor /proc/cpuinfo | wc -l" and type the number here, or stay with automatic configuration

events {
    worker_connections  1024; # use "ulimit -n" and type the number here
}

http {
############# NGINX conf
  include /etc/nginx/mime.types;
  include /etc/nginx/fastcgi.conf;

  sendfile     on;
  tcp_nopush   on;
  server_names_hash_bucket_size 128;

############## NGINX security
  server_tokens off;
  proxy_hide_header X-Powered-By;
  client_body_buffer_size 10K;
  client_header_buffer_size 1k;
  client_max_body_size 8M;
  large_client_header_buffers 4 8k;

  client_body_timeout 12;
  client_header_timeout 12;
  keepalive_timeout 15;
  send_timeout 10;

  gzip             on;
  gzip_comp_level  2;
  gzip_min_length  1000;
  gzip_proxied     expired no-cache no-store private auth;
  gzip_types       text/plain application/x-javascript text/xml text/css application/xml;

############# WP conf
  include /etc/nginx/wp.conf;
}

Fichier « fastcgi.conf » (/srv/docker/conf/nginx-wp/fastcgi.conf) / Utilisé par Nginx

fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
fastcgi_param  QUERY_STRING       $query_string;
fastcgi_param  REQUEST_METHOD     $request_method;
fastcgi_param  CONTENT_TYPE       $content_type;
fastcgi_param  CONTENT_LENGTH     $content_length;
fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;
fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

fastcgi_index  index.php;

fastcgi_param  REDIRECT_STATUS    200;

Fichier « mime.types » (/srv/docker/conf/nginx-wp/mime.types) / Utilisé par Nginx

types {
  text/html                             html htm shtml;
  text/css                              css;
  text/xml                              xml rss;
  image/gif                             gif;
  image/jpeg                            jpeg jpg;
  image/svg+xml                         svg svgz;
  application/x-javascript              js;
  text/plain                            txt;
  text/x-component                      htc;
  text/mathml                           mml;
  image/png                             png;
  image/x-icon                          ico;
  image/x-jng                           jng;
  image/vnd.wap.wbmp                    wbmp;
  application/java-archive              jar war ear;
  application/mac-binhex40              hqx;
  application/pdf                       pdf;
  application/x-cocoa                   cco;
  application/x-java-archive-diff       jardiff;
  application/x-java-jnlp-file          jnlp;
  application/x-makeself                run;
  application/x-perl                    pl pm;
  application/x-pilot                   prc pdb;
  application/x-rar-compressed          rar;
  application/x-redhat-package-manager  rpm;
  application/x-sea                     sea;
  application/x-shockwave-flash         swf;
  application/x-stuffit                 sit;
  application/x-tcl                     tcl tk;
  application/x-x509-ca-cert            der pem crt;
  application/x-xpinstall               xpi;
  application/zip                       zip;
  application/octet-stream              deb;
  application/octet-stream              bin exe dll;
  application/octet-stream              dmg;
  application/octet-stream              eot;
  application/octet-stream              iso img;
  application/octet-stream              msi msp msm;
  audio/mpeg                            mp3;
  audio/x-realaudio                     ra;
  video/mpeg                            mpeg mpg;
  video/quicktime                       mov;
  video/x-flv                           flv;
  video/x-msvideo                       avi;
  video/x-ms-wmv                        wmv;
  video/x-ms-asf                        asx asf;
  video/x-mng                           mng;
}

Avant dernier fichier de configuration, il concerne le serveur SQL (MariaDB dans l’exemple). Par défaut, MariaDB est plutôt bien optimisé et configuré ; toutefois, si vous utilisez cette stack sur des serveurs mutualisés (VPS), vous pourrez vite effondrer les performances de votre site web et avoir de grosses lenteurs.

Le fichier que je vous propose est exploité sur un VPS dôté d’ 1 vCPU et de 2 Go de RAM. Le « performance_schema » est actif, vous permettant d’avoir quelques métriques et des statistiques relatives à l’utilisation de la base de données. Par défaut, MariaDB utilise le moteur « InnoDB » : quelques paramètres sont apportés pour limiter le serveur, notamment pour optimiser les ressources et éviter la surcharge inutile.

fichier « /srv/docker/conf/custom-mysql.cnd » / utilisé par MariaDB :

; custom my.cnf for MySQL8 and WordPress website

[mysqld_safe]
socket = /var/run/mysqld/mysqld.sock
nice = 0

[mysqld]
performance_schema=ON
performance-schema-instrument='stage/%=ON'
performance-schema-consumer-events-stages-current=ON
performance-schema-consumer-events-stages-history=ON
performance-schema-consumer-events-stages-history-long=ON

; InnoDB tuning
innodb_file_per_table = 1
innodb_buffer_pool_size = 1G
innodb_log_file_size = 48M

; Cache
query_cache_type = 1
query_cache_size = 128M
query_cache_limit = 16M

; Misc
tmp_table_size = 64M
max_heap_table_size = 64M

; Logging
log_error = /var/log/mysql/error.log
slow_query_log_file = /var/log/mysql/mariadb-slow.log
long_query_time = 10
log_slow_rate_limit = 1000
expire_logs_days = 10
max_binlog_size = 100M

Enfin, un dernier fichier de configuration est nécessaire, le « php.ini ». Cet fichier permet d’affiner la configuration PHP, notamment pour éviter des erreurs et des plantages lors de l’administration de votre site web.

fichier « /srv/docker/conf/php.ini »

memory_limit = 256M
upload_max_filesize = 32M
post_max_size = 32M
open_basedir = /var/www/html:/tmp:/usr/local/lib/php
display_errors = 0
display_startup_errors = 0
register_globals = Off
allow_url_fopen = 0
allow_url_include = 0
expose_php = 0
file_uploads = On
max_execution_time = 600
max_input_time = 600
max_input_vars = 2000

Finalisation de la configuration WordPress

Bien entendu, n’oubliez pas de changer le nom de domaine pour correspondre à vos besoins. Lancez une première fois le stack – Une fois votre WordPress en place, installez l’extension « Redis Object Cache » (auteur = Till KRÜSS) et activez-là.

Modifiez le fichier « wp-config.php » qui se trouve dans le volume de données WordPress « /var/lib/docker/volumes/docker_datawp/_data/ » et ajoutez ces 5 lignes juste en dessous du premier commentaire :

define('WP_CACHE', true);
define('WP_REDIS_HOST', 'redis');
define('WP_REDIS_PORT', '6379');
define('WP_CACHE_KEY_SALT', 'B33?,;j3D$srtX-nJC[[email protected]!SjSRDL?S~lg{8>(pagVe|-QoX..1Ky3PkG.1|C');
define( 'WP_REDIS_PASSWORD', 'changemeWithALongPassword' );
define( 'WP_REDIS_TIMEOUT', 1 );
define( 'WP_REDIS_READ_TIMEOUT', 1 );
define( 'WP_REDIS_DATABASE', 0 );
define('WP_REDIS_DISABLE_BANNERS', 'true');

// suppression automatique des clés en cache après 7 jours
define( 'WP_REDIS_MAXTTL', 60 * 60 * 24 * 7 );

Vous pouvez enfin retourner dans l’interface d’administration de WordPress, notamment dans la page « Réglages » > « Redis » et cliquer sur le bouton « Enable Object Cache ».


Avec ce stack, vous avez un site web sous WordPress derrière Traefik et Nginx, en plus d’avoir du cache objet via REDIS.

Vous pouvez retrouver l’ensemble des fichiers sur GitHub à cette adresse.

6 réflexions sur “WordPress et Traefik – Mise en place d’une stack docker-compose”

  1. Superbe stack. Deux questions cependant :

    – Comment modifier le stack pour héberger plusieurs site sur un seul serveur ? Mon approche serait de dupliquer le stack MariaDB/Redis/Nginx/WP et d’ajouter une redirection au niveau de Traefik_dynamic.yml vers un autre port que le port 80 (site 1 sur le port 80, site 2 sur le port 81, etc). Cela se joue au niveau du routers & du services, mais comment exactement ?

    – Comment modifier le stack pour faire de l’hébergement local ? Mon approche ici serait de modifier la partie de traefik.yml pour utiliser un certificat généré par MKcert. Pareil, avez-vous déjà tenté ? Et si oui comment y êtes vous arrivé ?

    Merci,

    Benjamin

    1. Hi !
      Il est possible d’avoir plusieurs sites sur le même serveur, attention toutefois aux ressources nécessaires. Localement, j’utilise une seule VM pour tester mes stack docker-compose, toutes derrière Traefik. Pour se faire, côté DNS, tous les noms de domaines (DNS type A) doivent pointer vers l’IP du serveur/traefik. Dupliquez les fichiers/dossiers nécessaires pour mariaDB+Nginx+WordPress et modifiez les mots de passes, URL, variables pour les versions si besoin etc. Côté Traefik, tout se joue en effet dans le fichier traefik_dynamic.yml. Il faut dupliquer les « services » et les « routers » (en bas du traefik_dynamic.yml) et faire pointer chaque service/routeur aux nginx souhaités. Les redirection seront automa(g)tiques.
      Pas besoin de changer les ports des nginx, tous peuvent écouter sur le port 80, l’important étant que la configuration dynamique côté traefik soit cohérente et que les fichiers .conf de nginx soit à jour (changer l’url « server » et faire pointer vers les bons services/noms des conteneurs docker). Je pourrai ajouter ce genr de stack au dépôt GitHub…

      Concernant les certificats SSL gérés autrement que par Let’s Encrypt, c’est en effet possible de les générer via MKcert et les rendre opérant avec Traefik. J’en ai fais un article qui est encore en brouillon, mais voici un lien intéressant (sur lequel je me suis basé) : https://zestedesavoir.com/billets/3355/traefik-v2-https-ssl-en-localhost/

      Au plaisir ! 😉

    2. Merci beaucoup d’avoir pris le temps de répondre et pour les exemples.

      J’ai eu un problème en mettant en place le HTTPS local d’erreur 404 … car je n’entrais pas la bonne URL (vous avez dit boulet). Pour le reste, ça à l’air de marcher, mais je n’ai pas encore tout testé (non je ne dirais pas le temps que j’ai perdu sur cette erreur d’URL).

      Je verrais bien si je butte encore sur quelque chose. Mais c’est certain que cela simplifie furieusement la mise en place d’un WordPress de démo 🙂

      Bonne soirée/journée/après-midi/appétit si vous passez à table.

  2. Bonjour :
    Est ce que le chemin du fichier log précisé dans traefik.yml (filePath) ne doit pas être celui qui a été créé en tout début (mkdir et touch) ???

    Franck

    1. Bonjour,
      Non, le chemin saisit dans le fichier traefik.yml stipule l’emplacement du fichier dans le conteneur, conformément aux directives de Traefik Labs =) c’est pour ça que dans le fichier docker-compose, il y a un volume monté de la machine vers le conteneur (chemin_local_serveur:chemin_dans_le_conteneur)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *