This article essentially picks up on the WordPress on Docker (with nginx-mysql) post. It demonstrates how to host a WordPress website on Docker, with Nginx as the webserver (and SSL from LetsEncrypt), and a MySQL database. We’ll address a known issue with file permissions with WordPress files created or edited inside the Docker container, by mapping the user on the host (i.e. your computer) to the owner in the container.
Docker, WordPress, Nginx, LetsEncrypt
The following example works as is and host a live WordPress website on any Linux environments. I’m personally using a variation of it to host this website on a RaspberryPi 4. You’ll notice I rely heavily on ENV variables to avoid changing Docker files or other configuration files. Furthermore, I am requesting the SSL certificates from the Host with certbot (saving the two files in a certs/
in the root of the repo, on the host) and mapping them to the appropriate website directory.
Run this command to get your certificates from Certbot and follow the instructions on the terminal:
certbot certonly -i nginx -w ~/path/to/website
If, like me, you use Cloudflare to manage your DNS, once you’ve issued an API token and saved it to say cloudflare-api-token.ini
, in ~/.secrets/
directory for instance, you can issue the following command:
certbot certonly \
-i nginx \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/cloudflare-api-token.ini \
--dns-cloudflare-propagation-seconds 60 \
-w ~/path/to/website
Docker compose
We will require a service for each WordPress website, Nginx webserver, and MySQL database. So without any further ado, ecce my docker-compose.yml
file:
version: '3'
services:
front:
container_name: ${APP_PREFIX}wp
image: dockerhub/username:wordpress
build:
context: .
dockerfile: wp.dockerfile
args:
- UID=${UID:-(id -u)}
- GID=${GID:-(id -g)}
- WEBUSER=${WEBUSER:-(id -un)}
- WEBGROUP=${WEBGROUP:-(id -gn)}
restart: unless-stopped
ports:
- 9000
depends_on:
- database
networks:
- backend
- frontend
tty: true
volumes:
- wordpress:/var/www/html
- ./wp/wp-content/uploads:/var/www/html/wp-content/uploads:rw
- ./wp/wp-content/themes:/var/www/html/wp-content/themes:rw
- ./wp/wp-content/plugins:/var/www/html/wp-content/plugins:rw
#- ./php/:/usr/local/etc/php/conf.d/ # any custom PHP configuration
environment:
- WORDPRESS_DOMAIN="http://${APP_HOST}/"
- WORDPRESS_DB_HOST=database
- WORDPRESS_DB_NAME=${DB_NAME}
- WORDPRESS_DB_USER=${DB_USERNAME}
- WORDPRESS_DB_PASSWORD=${DB_PASSWORD}
- WORDPRESS_DEBUG=true
- WORDPRESS_CONFIG_EXTRA=
define('FS_METHOD', 'direct');
define('WP_HOME', 'https://${APP_HOST}/');
define('WP_SITEURL', 'https://${APP_HOST}/');
- TZ=Europe/London
database:
container_name: ${APP_PREFIX}db
image: dockerhub/username:wpdb
build:
context: .
dockerfile: db.dockerfile
restart: unless-stopped
networks:
- backend
command: [
"--explicit_defaults_for_timestamp"
]
volumes:
#- ./db:/docker-entrypoint-initdb.d/ # any SQL queries that run at build time (e.g. live DB export)
- dbdata:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${DB_NAME}
- MYSQL_USER=${DB_USERNAME}
- MYSQL_PASSWORD=${DB_PASSWORD}
- TZ=Europe/London
proxy:
container_name: ${APP_PREFIX}proxy
image: nginx
restart: unless-stopped
ports:
- 80:80
- 443:443
depends_on:
- front
networks:
- frontend
volumes_from:
- front
volumes:
- ./proxy:/etc/nginx/conf.d/:ro
- ./certs/:/etc/letsencrypt/live/${APP_HOST}:delegated
environment:
- NGINX_HOST=${APP_HOST}
- NGINX_PORT=443
- TZ=Europe/London
volumes:
wordpress:
dbdata:
networks:
frontend:
driver: bridge
backend:
driver: bridge
Dependencies
This setup depends on two Docker files – wp.dockerfile
and db.dockerfile
, and a .env
file (so we don’t have to hardcode stuff). The Docker file for the DB is the simplest, and that 2nd line is even optional:
FROM mysql:latest
RUN echo "Database Ready!"
WP Dockerfile
And here’s an exhaustive wp.dockerfile
file that addresses issues with permissions on files edited from inside the Docker container. In our custom Docker image we will create a shell user and group to match with the Host computer.
FROM wordpress:6.4-fpm
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini
ARG UID
ARG GID
ARG WEBUSER
ARG WEBGROUP
ARG WEBUSER=${WEBUSER}
ARG WEBGROUP=${WEBGROUP}
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 "Front-End Ready!"
ENV
And finally, you should only need to edit the .env
file to fit your requirements:
UID=1000
GID=1000
WEBUSER=host-user
WEBGROUP=host-group
APP_PREFIX=website-name
APP_HOST=${APP_PREFIX}.uk
[email protected]
DB_NAME=the-name-of-the-database
DB_USERNAME=user-with-access-to-db
DB_PASSWORD=your-password
DB_CHARSET=utf8mb4
DB_COLLATE=
MYSQL_ROOT_PASSWORD=S0meRand0mPass0rd
WORDPRESS_TABLE_PREFIX=wp_
WORDPRESS_DEBUG=1
WP_AUTO_UPDATE_CORE=0
Docker Volumes
Customise PHP
A volume on the WordPress container (commented out in the above example) allowing you to drop any .ini files in php
/ directory to customise things like upload limit, or execution times:
upload_max_filesize=30M;
memory_limit=256M;
error_reporting=E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED;
extension=mysqli;
max_execution_time=90;
Custom SQL commands (e.g. import database)
The commented out volume on the database
service allows for .sql files to be executed when the Docker image is built. If you require a new DB export, for instance, to be added to your Docker image, you may need to delete the old image from your computer before running docker compose up -d --force-recreate --build
command.
Nginx configuration
You’ll also need to mount the nginx configuration for your domain, from the directory proxy/
in the root of the repo. Call the file whatever you want as long as it has the .conf
extension. 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).
nginx configuration (e.g. `website.conf`):
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
Run the setup with the following command:
docker compose up -d --build [--force-recreate] [--remove-orphans]
Happy coding!