Wordpress et docker swarm
Posted on sam. 31 mai 2025 in devops
In an effort to cut down on the cost of my too-numerous servers, I decided to decommission one of my OVH servers. Among other things, this server hosts a WordPress site that I plan to migrate to another server.
Small problem: I don’t want to install the entire PHP/FPM stack on a nice clean Debian. Luckily for me, that’s exactly (not at all) why Docker was created. And even more luckily, it turns out WordPress ships its own Docker image, so I won’t even have to get my hands dirty. How happy (and naïve) I am.
After struggling for quite a while to get everything working properly, I decided to document the entire stack here, just in case it might help another lost soul.
First Approach
Okay, so I just need to go from an architecture where an Nginx calls a PHP-FPM daemon, to one where an Nginx calls a PHP-FPM daemon inside a Docker container. Simple. This would also be a good time to fully dockerize everything, starting with the WordPress files into Docker volumes and the SSL certificates into Docker configs. (We’ll skip over the part where spinning up a Docker container with MySQL is easy enough to gloss over.)
I’ll spare you the reverse engineering it took for me to understand how WordPress works. Whether it’s the PHP-FPM daemon or the Dockerized version, it needs an Nginx or Apache to serve static files. A daemon or Docker container alone will never get the job done.
But even then, we’re still facing several problems:
-
Docker “supports IPv6”. But you really need to emphasize the quotation marks (meaning, no, it doesn’t really support it). If I want to maintain connectivity, I’ll need an HTTP proxy outside of Docker.
-
WordPress makes “loopback” requests. Those are requests where it tries to crawl itself in order to trigger cron jobs (very elegant).
Increased Complexity
After a lot of headaches trying to figure out why it doesn’t work out of the box, I’ve come to the conclusion that it just can’t be simple, and the stack will have to be made more complex.
As a side note, this is one of those moments in IT where things that should work together effortlessly just don’t. So we keep adding layers of complexity until it finally works. And that’s when you get that bittersweet feeling of satisfaction from having succeeded, mixed with disappointment at the ugliness of the solution.
So, to go into a bit more detail on this new implementation, we now have:
- An HTTP proxy that handles SSL validation using certificates available on the machine’s disk.
- A Dockerized Nginx capable of responding to a loopback request. For that, it needs to:
- Be resolvable via WordPress’s public address (see the hostname in the Docker config below).
- Have access to the same SSL certificates used by the proxy (here done via Docker configs, but it could also be through a volume like the rest).
Which gives us the following configuration:
services:
my-wp-nginx:
hostname: www.my-wp.com # Wordpress public address
image: nginx
networks:
- ntw_nginx
ports: ["0.0.0.0:8101:80"] # port referenced in the proxy config
deploy:
mode: replicated
replicas: 4
configs:
- source: nginx_config
target: /etc/nginx/nginx.conf # nginx config shown afterwards which will do the redirection toward PHP-FPM (the docker below)
- 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 # hostname to be referenced by the nginx config
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
It’s unfortunate that WordPress (or rather its official Docker image) cannot be used on its own. The fact that you need two separate pieces of software to successfully run WordPress isn’t clearly stated in their documentation or examples.
Managing configs with Docker Swarm is, to put it politely, awkward. You can’t update a Docker config directly. You have to deploy a new one with a non-conflicting name, update the container, and then clean up the orphaned configs. I’ve scripted this heavily, because it gets really annoying by the second typo in an Nginx configuration.
But after all the hassle, this setup works well for me, and has the nice advantage of being able to scale fairly easily.
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