fr en

Wordpress & docker swarm

Posted on sam. 31 mai 2025 in devops

Dans un effort de limiter les coûts de mes trop nombreux serveurs, je me suis mis en tête de dépeupler un de mes serveurs OVH en vue de la décommissionner. Entre autre chose, ce serveur héberge un Wordpress que je vais rapatrier sur un autre serveur.

Petit problème, je ne veux pas installer toute la stack PHP/FPM sur une belle debian toute propre. Heureusement pour moi, c’est exactement (pas du tout) pour ça que docker a été créé. Et encore heureusement pour moi il se trouve que wordpress ship sa propre image docker et je n’aurai même pas à mettre les mains dans le cambouis Que je suis heureux (et naïf).

Après avoir longtemps lutté pour faire fonctionner proprement tout ça, j’ai décidé de documenter toute la stack ici. Des fois que ça aiderait une autre âme perdue.

Première approche

Ok donc il suffit que je passe d’une architecture où un nginx fait appel à un démon php fpm, à une architecture où un nginx fait appel à un démon PHP-FPM dans un docker. Simple. Ça serait aussi le moment de tout dockeriser, à commencer par les fichiers wordpress à mettre dans un docker volumes et des certificats ssl à mettre dans un docker configs. (On va considérer que le montage d’un docker avec MySQL est assez simple pour être passé en ellipse)

Je vous passe la rétro ingéniérie nécessaire à ma compréhension du fonctionnement de Wordpress. Que ce soit le démon avec PHP-FPM ou dockerisé, il a besoin d’un nginx ou d’un apache pour servir les fichiers statics. Un process php seul ne fera jamais le taff.

Mais même ainsi, on reste face à plusieurs problèmes :

  • docker “supporte l’IPv6”. Mais il faut vraiment insister sur les guillemets (dans le sens où, non il ne le supporte pas), si je veux garder la connectivité il va me falloir un proxy http hors de docker.
  • wordpress fait des requêtes de “loopback”. Ce sont des requêtes où il tente de se crawl lui même pour déclencher des cron (très élégants)

Complexification

Après beaucoup de prise de tête à comprendre pourquoi ça ne marche pas simplement, j’en arrive à la conclusion que ça ne pourra pas être simple et qu’il va falloir complexifier la stack.

Aparté, c’est un de ces moments en informatique où les choses qui se conçoivent simplement et qui devaient s’emboîter sans effort coincent. Alors on rajoute des couches de complexité jusqu’à ce que ça fonctionne. C’est alors qu’on éprouve un sentiment doux-amer de satisfaction d’avoir réussi, mêlée à la déception face à la laideur de la solution…

Donc pour rentrer un peu dans le détails de cette nouvelle implémentation on a :

  • un proxy http qui fait la validation SSL avec les certificats disponibles sur le disque de la machine
  • un nginx dockerisé capable de répondre à une requête de loopback. Pour cela il lui faut :
    • être résolvable avec l’adresse publique du wordpress (voir le hostname dans la configuration docker ci-dessous)
    • avoir accès aux certificats SSL aussi utilisés par le proxy (ici fait via docker configs mais ça peut être via un volume comme le reste)

Ce qui nous donne la configuration suivante :

services:
  my-wp-nginx:
    hostname: www.my-wp.com  # l'adresse publique du wordpress
    image: nginx
    networks:
    - ntw_nginx
    ports: ["0.0.0.0:8101:80"]  # le port à référencer dans la config du proxy
    deploy:
      mode: replicated
      replicas: 4
    configs:
    - source: nginx_config
      target: /etc/nginx/nginx.conf  # la config nginx référencée après, qui redirigera vers fpm
    - source: wp_ssl_fullchain
      target: /etc/fullchain.pem
    - source: wp_ssl_privkey
      target: /etc/privkey.pem
    volumes:
    - mywpfiles:/var/www/html

  my-wp-fpm:
    image: wordpress:6.7-php8.2-fpm
    hostname: my-wp.fpm  # le nom résolvable par nginx pour cibler le conteneur php
    networks:
      - ntw_nginx
      - ntw_mysql
      - ntw_redis
    deploy:
      mode: replicated
      replicas: 2
    environment:
      WORDPRESS_DB_HOST: mysql.db
      WORDPRESS_DB_USER: my-wp
      WORDPRESS_DB_NAME: my-wp
      WORDPRESS_DB_PASSWORD: my-wp-pwd
    volumes:
    - mywpfiles:/var/www/html

volumes:
  mywpfiles:
    name: mywpfiles
Nginx config
server {
    listen 443 ssl;
    ssl_certificate /etc/fullchain.pem;
    ssl_certificate_key /etc/privkey.pem;
    server_name www.my-wp.com;

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

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

    location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2?|eot|ttf|svg|mp4)$ {
        expires max;
        log_not_found off;
        access_log off;
        try_files $uri =404;
    }

    # Send PHP requests to PHP-FPM
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass my-wp.fpm:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; # Use the path INSIDE the FPM container
    }
}

Conclusion

Il est regrettable que le wordpress (ou bien le docker officiel) soit incapable d’être utilisé seul. Le fait qu’il faille deux logiciels pour réussir à faire tourner Wordpress n’est pas vraiment explicite dans leur documentation ou dans leurs exemples.

La gestion des configs avec docker swarm est mal-aisée pour être poli. On ne peut pas mettre à jour une config docker, donc il faut en déployer une nouvelle avec un nom qui ne collisionne pas, mettre à jour le container puis ensuite nettoyer les configs orphelines. J’ai violement scripté ça, parce que ça devient pénible à la deuxième typo dans une configuration nginx.

Mais après en avoir sué, cette configuration me convient et a le bon goût de pouvoir monter en charge assez facilement.

Annexe

Code for chart 1
flowchart TD
    subgraph WebServer["WebServer"]
        disk["Disk"]
        nginx["nginx"]
        configs["SSL Certs"]
        FPM["wordpress dockerized"]
        SQL["MySQL"]
    end
    I["Internet"] <-- ipv4 & ipv6 --> nginx
    nginx -- php exec --> FPM
    nginx --> configs
    FPM -- db conn --> SQL
    FPM -- read code --> disk
    nginx -- read static files --> disk
    SQL --> disk

    nginx@{ shape: procs}
    FPM@{ shape: procs}
    SQL@{ shape: db}
    disk@{ shape: disk}
    I@{ shape: rounded}

     I:::Sky
    classDef Sky stroke-width:1px, stroke-dasharray:none, stroke:#374D7C, fill:#E2EBFF, color:#374D7C
Code for chart 2
flowchart TD
    subgraph Dockers["Dockers"]
        nginx["nginx"]
        configs["configs"]
        FPM["wordpress dockerized"]
        SQL["MySQL"]
        volumes["volumes"]
    end
    subgraph WebServer["WebServer"]
        Dockers
        disk["Disk"]
    end

    I["Internet"] <-- ipv4 & ipv6 --> nginx
    nginx -- php exec --> FPM
    nginx -- read SSL certs --> configs
    FPM -- db conn --> SQL
    FPM -- read code --> volumes
    nginx -- read static files --> volumes
    volumes --> disk
    SQL --> volumes

    nginx@{ shape: procs}
    FPM@{ shape: procs}
    SQL@{ shape: db}
    disk@{ shape: disk}
    I@{ shape: rounded}
    I:::Sky

    classDef Sky stroke-width:1px, stroke-dasharray:none, stroke:#374D7C, fill:#E2EBFF, color:#374D7C
Code for chart 3
flowchart TD
    subgraph Dockers["Dockers"]
        nginx["nginx"]
        configs["configs"]
        FPM["wordpress dockerized"]
        SQL["MySQL"]
        volumes["volumes"]
    end
    subgraph WebServer["WebServer"]
        proxy["http proxy"]
        Dockers
        disk["Disk"]
        SSL["SSLCerts"]
    end
    I["Internet"] <-- ipv4 & ipv6 --> proxy
    proxy --> nginx
    proxy -- SSL Certs --> SSL
    SSL -- stored --> disk
    SSL -- loaded --> configs
    nginx -- php exec --> FPM
    FPM -- loopback request --> nginx
    nginx -- access for loopback --> configs
    FPM -- db conn --> SQL
    FPM -- read code --> volumes
    nginx -- read static files --> volumes
    volumes --> disk
    SQL --> volumes

    nginx@{ shape: procs}
    FPM@{ shape: procs}
    SQL@{ shape: db}
    disk@{ shape: disk}
    I@{ shape: rounded}

    I:::Sky
    classDef Sky stroke-width:1px, stroke-dasharray:none, stroke:#374D7C, fill:#E2EBFF, color:#374D7C