Synopsis
This article describes how to configure and deploy a WordPress instance, either in production or for local development (and assumes a beginner+ / enthusiast+ level in DevOps).
To achieve this, other than the official WordPress codebase, we will also need a database and a web server which we’ll refer to as services
(the Docker Compose terminology) and each of these will run in their own Docker container*:
- WordPress (v6.4)
- Database (MySQL v8.0)
- Proxy (Nginx v20)
* Note
This separation is not required! The project can be installed and deployed on a single server running all services, however this is not ideal and definitely not an industry standard. Apart from the obvious separation of concerns, we’re also gaining atomicity by separating our services while requiring less processing power so that when deploying to cloud for instance, we can provision a cheaper infrastructure to run on.
Considerations
We want to employ volumes
to achieve data persistence, and networks
to segregate traffic and data access. The networks
will connect the proxy
and, respectively, database
services to our WordPress installation, but not to eachother; one benefit of this is that the database is not publicly available.
Basic WordPress on Docker
The following example is a basic docker-compose.yml
template:
version: '3'
services:
frontend:
image: wordpress:6.4-fpm
restart: unless-stopped
ports:
- 9000:9000
depends_on:
- database
networks:
- public
- internal
volumes:
- wordpress:/var/www/html
environment:
- WORDPRESS_DOMAIN="https://website.uk/"
- WORDPRESS_DB_HOST=database
- WORDPRESS_DB_PORT=3306
- WORDPRESS_DB_NAME=${DB_NAME:-wpdatabase}
- WORDPRESS_DB_USER=${DB_USERNAME:-wpdbuser}
- WORDPRESS_DB_PASSWORD=${DB_PASSWORD:-wpdbpassword}
database:
image: mysql:latest
restart: unless-stopped
networks:
- internal
volumes:
- dbdata:/var/lib/mysql
environment:
- MYSQL_DATABASE=${DB_NAME:-wpdatabase}
- MYSQL_USER=${DB_USERNAME:-wpdbuser}
- MYSQL_PASSWORD=${DB_PASSWORD:-wpdbpassword}
proxy:
image: nginx:latest
restart: unless-stopped
ports:
- 80:80
- 443:443
networks:
- public
depends_on:
- frontend
volumes_from:
- frontend
volumes:
wordpress:
dbdata:
networks:
public:
driver: bridge
internal:
driver: bridge
The above relies on a couple of environment variables, which you may want to define in a .env
file (rather than hardcoding them in the docker-compose.yml
file and version commit them):
DB_NAME=the-name-of-the-database
DB_USERNAME=user-with-access-to-db
DB_PASSWORD=your-password
# DB_PORT=3306
Nginx configuration
You’ll also need to mount the nginx configuration for your domain, from the directory proxy/
in the root of the repo.
You’ll also need to configure Nginx for your domain name. Create a file with .conf
extension in the proxy/
directory (which will mount it to the web proxy Docker container). Unfortunately you’ll need to hard-code a few things since we cannot use environment variables in Nginx configuration files (at least I’ve not been able to get it to work with the .env
file). Here’s an example (e.g. `website.conf`) with duplicated blocks for HTTP and HTTPS, in case you run into issues with SSL certificates you can rely on the good ol’ HTTP:
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name website.uk www.website.uk;
server_tokens off;
ssl_certificate /etc/letsencrypt/live/website.uk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/website.uk/privkey.pem;
root /var/www/html;
index index.php;
location ~ /.well-known/acme-challenge {
allow all;
root /var/www/html;
}
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass WORDPRESS-CONTAINER-NAME:9000;
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_intercept_errors on;
}
}
server {
listen 80;
listen [::]:80;
server_name website.uk www.website.uk;
server_tokens off;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass WORDPRESS-CONTAINER-NAME:9000;
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_intercept_errors on;
}
}
Conclusions
This is more than plenty to get you started with a fresh WordPress installation with Docker Compose. However there are things we can improve (e.g. persistence of important files such as WP themes, uploads, or plugins), as well as addressing potential issues which one might run into when developing locally (especially on MacOS, or Windows’ Subsystem for Linux WSL) particularly around file ownership and permissions between the host system and the Docker containers. We would probably also want to enable SSL on our website so that web browsers won’t complain.
;tldr
See my GitHub gist:
“Run WordPress website behind nGinx, on Docker, with SSL via Lets Encrypt“
https://gist.github.com/mariuscucuruz/530fd356e97146ece62be4e093b8a582
Improve on the basics
The above example creates a docker container running WordPress and persists data via named volumes, however we may encounter some challenges, for instance, when porting our local changes to staging or production. To persist and track our customisations we could either build (and push) our custom Docker images, or use Docker volumes (and track changes via Git or any versioning system), or both. The latter option is easier to implement if we were to build on the initial setup, so we’ll begin with that.
Persist your uploads, and track changes to Themes and Plugins
What we want to achieve is mapping certain files from the WordPress container onto our host computer so that Git will pick up and track all of our changes. In particular we’re interested in our Uploads and Themes, and quite possibly the Plugins we’ve installed. So we’ll add the local paths to the wordpress
volume we’ve defined, mapping them to the path inside the Docker container:
services:
...
frontend:
...
volumes:
- wordpress:/var/www/html
- ./wp-content/uploads:/var/www/html/wp-content/uploads:rw
- ./wp-content/themes:/var/www/html/wp-content/themes:rw
- ./wp-content/plugins:/var/www/html/wp-content/plugins:rw
Note
If other files are of interest to your case, you may add them to the volumes list. Be aware that the host paths (i.e. the first path, to the left of the colon :
) should be relative to the context, where the root
is where your docker-compose.yml
resides, and should almost always start with ./
.
Save changes to custom Docker image
To build on the official WordPress image for Docker and push the custom image to your Docker Hub you’ll need to create a new file alongside our docker-compose.yml
, call it wp.dockerfile
:
FROM wordpress:6.4-fpm
RUN echo "WordPress Ready!"
And if we update our docker-compose.yml
to use this new file (and mention our Docker Hub account while we’re at it):
services:
frontend:
...
build:
context: .
dockerfile: wp.dockerfile
image: dockerhub-username/wordpress:local
...
What this will do is allow us to build
our container, and push our custom WordPress image to our Docker Hub (you’ll need a terminal session where the Docker service is logged into your account). So the next time you spin the containers up you can add the --build
flag:
docker compose up -d --build [--force-recreate] [--remove-orphans]
Or just run docker compose build
. In the above example where image: dockerhub-username/wordpress:local
, use colon :
to tag different versions of your site (e.g. local
, dev
, staging
, prod
, live
etc.) to avoid deploying the wrong image to production, for instance.
Then, assuming you’re logged into your Docker Hub account, push the image by running
docker composer push
Similarly should work for the database
service to track pages and posts. We would need a very basic db.dockerfile
file
FROM mysql:latest
RUN echo "Database Ready!"
which we then reference in the docker-compose.yml
file:
services:
...
database:
...
build:
context: .
dockerfile: db.Dockerfile
image: dockerhub-username/wp-db:local
....
Then build and push your changes as Docker images with:
docker compose build
docker compose push
Permissions Issues
You may run into permissions issues and occasional warnings when uploading or saving content via WP Admin. These are generally caused by discrepancies between the session user on the host and the container and should not occur in production. To fix this we’ll need to map our local user name, group and ID to the WordPress container, in the wp.dockerfile
we’ve created earlier. Hold on to your hats cause this is gonna get bumpy:
FROM wordpress:6.4-fpm
ARG UID
ARG GID
ARG WEBUSER
ARG WEBGROUP
# try the ENV vars or default
ARG WEBUSER=${WEBUSER:-yourHostUserName}
ARG WEBGROUP=${WEBGROUP:-yourHostGroupName}
ARG UID=${UID:-1000}
ARG GID=${GID:-1000}
# create matching user and group
RUN addgroup --gid ${GID} --system ${WEBUSER}
RUN adduser --disabled-password --gecos '' --uid ${UID} --ingroup ${WEBGROUP} ${WEBUSER}
RUN chown -R ${WEBUSER}:${WEBGROUP} .
# update PHP-FPM user
RUN sed -ri -e "s!user = www-data!user = ${WEBUSER}!g" /usr/local/etc/php-fpm.d/www.conf
RUN sed -ri -e "s!group = www-data!group = ${WEBGROUP}!g" /usr/local/etc/php-fpm.d/www.conf
WORKDIR /var/www/html
RUN echo "WordPress Ready!"
Sure there are a few extra lines in there, but we would rather use the .env
file than hardcode our username and use id into the Dockerfile. So now the environments variable we need are:
UID=1000
GID=1000
WEBUSER=yourHostUserName
WEBGROUP=yourHostGroupName
DB_PORT=3306
DB_HOST=database
DB_NAME=the-name-of-the-database
DB_USERNAME=user-with-access-to-db
DB_PASSWORD=your-password
How do I find my username, user id, or group name? Well, on Unix terminals it’s usually the id
command in conjuncture with -u
or -g
flags to get ether the user or group IDs; for the actual names add -n
flag into the mix:
id -u # user id
id -g # group id
id -u -n # user name
id -g -n # group name
Bonus
On most Linux distributions you can also implement this in your docker-compose.yml
file to circumvent the need for new environment variables (but in my experience that does not always work, oddly):
services:
...
wordpress:
...
build:
context: .
dockerfile: wp.dockerfile
args:
- UID=${UID:-(id -u)}
- GID=${GID:-(id -g)}
- WEBUSER=${WEBUSER:-(id -un)}
- WEBGROUP=${WEBGROUP:-(id -gn)}
image: dockerhub-username/wordpress:local
....
GitHub Gist:
gist.github.com/mariuscucuruz
Hope you’ve found it useful.
Cheers