London, United Kingdom

(+44) 07788.377.460 [email protected]

Advanced WordPress on Docker with SSL on Nginx

WordPress on Docker with ssl on nginx

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!