Hosting Wordpress in Docker with SSL 2020
By: Zachary Tyhacz
Tags: Ssl, Docker Compose, Docker, Certbot, WordPress

How life is when you fully host your Wordpress site in docker successfully.

Wordpress is very popular these days, and so is Docker. Combining the two gives you a ton of flexibility such as moving your client’s Wordpress site to anywhere very quickly and having an easy way to spin it back up on a new server or VPS.

I have tried to follow other online tutorials on how to achieve this, but many are outdated.

In this article, I will walk you through how to conquer a fully hosted Wordpress site with a database, NGINX web server, and Certbot for SSL all in one docker-compose setup.

1. Initial Setup

When I have to host a Wordpress site, I usually have to include my own theme, plugins, and sometimes database data to make the site work the way it is supposed to. These are all completely possible to do with docker-compose in this article, but we will only be focusing on integrating our own theme and plugins into Wordpress.

I recommend having your project in a git repo and structured similarly to Wordpress’s folder structure. When we create our docker volumes, we want to easily move whole folders into our Wordpress instance instead of files.

This will make sense later, but if you already know the Wordpress folder structure you will know what to do.

2. Setup Docker Compose File

Let’s get cooking! Setting up Wordpress with docker-compose is super easy.

To fully host Wordpress, we need a few services:

Wordpress ( duh )

MySQL Database

Nginx

We will add Certbot Let’s Encrypt SSL later toward the end, this is the basic setup for HTTP.

In the root of your project repo, let’s begin our docker-compose file:

The # are descriptive comments to help you!

$ vim docker-compose.yml

# file name: docker-compose.yml

# this setup may not work correctly in other major versions!!!

version: '3' services:

db:

image: mysql:8.0

# name our containers so we can easily reference them

container_name: db

restart: unless-stopped

# if you use mysql version 8 you need PHP to handle

# passwords correctly

command: '--default-authentication-plugin=mysql_native_password' wordpress:

image: wordpress:5-fpm-alpine

# wordpress must wait for a database connection

depends_on:

- db



container_name: wordpress

restart: unless-stopped webserver:

# nginx needs wordpress started first

depends_on:

- wordpress

image: nginx:1.15.12-alpine

container_name: webserver

restart: unless-stopped

# open nginx's ports to access our site!!!

# you can of course change your ports for development,

# just make sure you listen to them in nginx

ports:

- "80:80"

- "443:443" # for https later!

So far we just added our initial basic setup. Next we will add our environmental variables.

3. Environmental Variables

There’s many ways to do this, but to keep our docker-compose secure while being in a repo, we will reference a .env file and use it’s values as variables within our docker-compose.yml

Make sure your .gitignore ignores your .env file!

# our .env

MYSQL_ROOT_PASSWORD=root_password

MYSQL_USER=wordpress

MYSQL_PASSWORD=wordpress_password

Pretty self explanatory. Our MySQL service needs initial login data for the root user and a user that Wordpress with login with. Now let’s pass some environmental variables to our docker compose setup:

version: '3'

services: db:

image: mysql:8.0

container_name: db

restart: unless-stopped

command: '--default-authentication-plugin=mysql_native_password'

# make sure to add path to env file.

# The MYSQL_* will automatically be passed

env_file: .env

environment:

- MYSQL_DATABASE=wordpress wordpress:

image: wordpress:5-fpm-alpine

depends_on:

- db

container_name: wordpress

restart: unless-stopped # make sure to add path to env file

env_file: .env

# we refence .env variables like `$MYSQL_USER`

environment:

- WORDPRESS_DB_HOST=db:3306

- WORDPRESS_DB_USER=$MYSQL_USER

- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD

- WORDPRESS_DB_NAME=wordpress webserver:

depends_on:

- wordpress

image: nginx:1.15.12-alpine

container_name: webserver

restart: unless-stopped

ports:

- "80:80"

- "443:443"

4. Mounting Volumes

With our custom theme files, we need to move them to the correct place in our Wordpress service instance’s files. Themes are stored in Wordpress’s /var/www/html/wp-content/themes/ folder, so we want to move our theme here. We are also going to add our initial HTTP nginx config to get our Wordpress hosted ( this is required for certbot and SSL certificate creation later ). If you have a plugins folder to add, add another line but replace themes with plugins .

Let’s add our theme files to our Wordpress service:

wordpress:

image: wordpress:5-fpm-alpine

depends_on:

- db

container_name: wordpress

restart: unless-stopped

volumes:

# we are going to save our wordpress data in our file system

- wordpress:/var/www/html

# we are going to put our custom theme into the themes folder

- /path/to/repo/myTheme/:/var/www/html/wp-content/themes/myTheme



env_file: .env

environment:

- WORDPRESS_DB_HOST=db:3306

- WORDPRESS_DB_USER=$MYSQL_USER

- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD

- WORDPRESS_DB_NAME=wordpress

And we’re going to create a volume for our database data so it can persist on our system:

db:

image: mysql:8.0

container_name: db

restart: unless-stopped

command: '--default-authentication-plugin=mysql_native_password'

env_file: .env

environment:

- MYSQL_DATABASE=wordpress



# we are going to save our database data in our file system

# using these named volumes will default to somewhere in docker files on your system

volumes:

- dbdata:/var/lib/mysql

And let’s add our HTTP nginx config and mount it onto our nginx service:

# in the root of your repo, nginx-conf/nginx.conf

server {

listen 80;

listen [::]:80;

server_name domain.com # add your domain name(s)server_name domain.com www. domain.com; index index.php index.html index.htm; # we will mount our wordpress volume to this root

# path so it will serve ( see below )

root /var/www/html; # this is for certbot https certifying later

location ~ /.well-known/acme-challenge {

allow all;

root /var/www/html;

} location ~ \.php$ {

try_files $uri =404;

fastcgi_split_path_info ^(.+\.php)(/.+)$;



# proxy pass to the container name and port

# we didn't expose wordpress's 9000 port to our host,

# but wordpress still 'exposes' a port between the

# containers

fastcgi_pass wordpress:9000; fastcgi_index index.php;

include fastcgi_params;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

fastcgi_param PATH_INFO $fastcgi_path_info;

} location ~ /\.ht {

deny all;

} location = /favicon.ico {

log_not_found off; access_log off;

}

location = /robots.txt {

log_not_found off; access_log off; allow all;

}

location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {

expires max;

log_not_found off;

}

}

And mount this config volume to nginx:

webserver:

depends_on:

- wordpress

image: nginx:1.15.12-alpine

container_name: webserver

restart: unless-stopped

ports:

- "80:80"

- "443:443"

volumes:

# we will also mount wordpress to nginx's

# html folder to easily serve it

- wordpress:/var/www/html

# we need to add our own nginx-config folder

- /path/to/repo/nginx-conf/:/etc/nginx/conf.d

All we have to do now is add our anonymous volumes to the bottom of our docker-compose.yml file. This is what it should look like:

version: '3'

services: db:

image: mysql:8.0

container_name: db

restart: unless-stopped

command: '--default-authentication-plugin=mysql_native_password'

env_file: .env

environment:

- MYSQL_DATABASE=wordpress

volumes:

- dbdata:/var/lib/mysql wordpress:

image: wordpress:5-fpm-alpine

depends_on:

- db

container_name: wordpress

restart: unless-stopped

volumes:

- wordpress:/var/www/html

- /path/to/repo/myTheme/:/var/www/html/wp-content/themes/myTheme env_file: .env

environment:

- WORDPRESS_DB_HOST=db:3306

- WORDPRESS_DB_USER=$MYSQL_USER

- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD

- WORDPRESS_DB_NAME=wordpress webserver:

depends_on:

- wordpress

image: nginx:1.15.12-alpine

container_name: webserver

restart: unless-stopped

ports:

- "80:80"

- "443:443"

volumes:

- wordpress:/var/www/html

- /path/to/repo/nginx-conf-folder/:/etc/nginx/conf.d # add this volumes section here <==================

volumes:

wordpress:

dbdata:

Go ahead and try running it!

$ docker-compose up

See the logs and check for any errors like DB Connection issues from Wordpress which may be caused by incorrect or missing env vars. A restart or two could also fix some issues.

Fixing Any Permission Issues

I’ve had issues with my mounted theme in Wordpress not working right with images not showing or just looking broken. Files and folders require specific permission modes and owner.

The Wordpress container has it’s own user that will own Wordpress files which I’ve found to be either 82 or http . You can figure this out by searching the docker volume files in:

/var/lib/docker/volumes/src_wordpress/_data and seeing the owner of the files. In these commands below, I’m going to use the http user which is what was used on my Linux server.

Correct file and folder permissions:

# correct permissions for all files

$ sudo find /path/to/your/theme/folder/ -type f -exec chmod u=rw,g=r,o=r {} +` # correct permissions for all directories

$ sudo find /path/to/your/theme/folder/ -type d -exec chmod u=rwx,g=rx,o=rx {} +` # correct ownership

$ sudo chown -R http:http /path/to/your/theme/folder/

5. Getting SSL Certificate

We’re so close! To get an SSL certificate, we need to add Certbot as a service to our docker-compose then do 3 steps:

While Nginx is running the HTTP config, we run a Certbot command to get certificate. We swap the Nginx configuration with an HTTPS config Restart Nginx container

Let’s setup our docker-compose for certbot:



webserver:

depends_on:

- wordpress

image: nginx:1.15.12-alpine

container_name: webserver

restart: unless-stopped

ports:

- "80:80"

- "443:443"

volumes:

- wordpress:/var/www/html

- /path/to/nginx-conf-folder/:/etc/nginx/conf.d

# nginx will need the certificate files certbot will create

- certbotdata:/etc/letsencrypt

networks:

- app-network



certbot:

depends_on:

- webserver

image: certbot/certbot

container_name: certbot

volumes:

# we save our directory of keys on our host server

- certbotdata:/etc/letsencrypt

# we bind our wordpress site to the html root

- wordpress:/var/www/html

# this command will generate the certificate.

# make sure to change your -d and --email flags

command: certonly --webroot --webroot-path=/var/www/html --email



# add the certbotdata volume down here:

volumes:

wordpress:

dbdata:

certbotdata: ^... ... ...webserver:depends_on:- wordpressimage: nginx:1.15.12-alpinecontainer_name: webserverrestart: unless-stoppedports:- "80:80"- "443:443"volumes:- wordpress:/var/www/html- /path/to/nginx-conf-folder/:/etc/nginx/conf.d# nginx will need the certificate files certbot will create- certbotdata:/etc/letsencryptnetworks:- app-networkcertbot:depends_on:- webserverimage: certbot/certbotcontainer_name: certbotvolumes:# we save our directory of keys on our host server- certbotdata:/etc/letsencrypt# we bind our wordpress site to the html root- wordpress:/var/www/html# this command will generate the certificate.# make sure to change your -d and --email flagscommand: certonly --webroot --webroot-path=/var/www/html --email youremail@email.com --agree-tos --no-eff-email --force-renewal -d domain.com -d www.domain.com # add the certbotdata volume down here:volumes:wordpress:dbdata:certbotdata:

Awesome, now let’s run our Certbot container to get our SSL certificate:

$ docker-compose up --no-deps certbot

If you see a Congratulations in the logs saying that it successfully generated the certificate(s) for you. If it failed, make sure Nginx is running and that your DNS settings are correct. And double check the certbot command: in your docker-compose.yml .

Now, we have to swap our Nginx config to run SSL. Let’s edit our nginx-conf/nginx.conf for SSL:

# nginx-conf/nginx.conf

# redirect to HTTPS ( optional )

server {

listen 80;

listen [::]:80; server_name domain.com www.domain.com location ~ /.well-known/acme-challenge {

allow all;

root /var/www/html;

}

rewrite ^

}

} location / {rewrite ^ https://$host$request_uri ? permanent; server {

listen 443 ssl http2;

listen [::]:443 ssl http2; server_name domain.com www.domain.com index index.php index.html index.htm; root /var/www/html; server_tokens off; # add our paths for the certificates Certbot created

ssl_certificate /etc/letsencrypt/live/domain.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/www.domain.com/privkey.pem; # some security headers ( optional )

add_header X-Frame-Options "SAMEORIGIN" always;

add_header X-XSS-Protection "1; mode=block" always;

add_header X-Content-Type-Options "nosniff" always;

add_header Referrer-Policy "no-referrer-when-downgrade" always;

add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always; location / {

try_files $uri $uri/ /index.php$is_args$args;

} location ~ \.php$ {

try_files $uri =404;

fastcgi_split_path_info ^(.+\.php)(/.+)$;

fastcgi_pass wordpress:9000;

fastcgi_index index.php;

include fastcgi_params;

fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

fastcgi_param PATH_INFO $fastcgi_path_info;

} location ~ /\.ht {

deny all;

} location = /favicon.ico {

log_not_found off; access_log off;

} location = /favicon.svg {

log_not_found off; access_log off;

} location = /robots.txt {

log_not_found off; access_log off; allow all;

} location ~* \.(css|gif|ico|jpeg|jpg|js|png)$ {

expires max;

log_not_found off;

}

}

Now, let’s restart Nginx so the new SSL config can take effect:

$ docker-compose up -d --force-recreate --no-deps webserver

And to prevent Renewal spam limits, comment out the command under Certbot in your docker-compose.yml :



# command: certonly --webroot --webroot-path=/var/www/html --email # comment out with '#'# command: certonly --webroot --webroot-path=/var/www/html --email youremail@email.com --agree-tos --no-eff-email --force-renewal -d domain.com -d www.domain.com

Conclusion

Now, you’re done! That’s Wordpress fully hosted with SSL in Docker! It was a little much, but we got it working. Here’s the full docker-compose.yml -

version: '3'

services: db:

image: mysql:8.0

container_name: db

restart: unless-stopped

command: '--default-authentication-plugin=mysql_native_password'

env_file: .env

environment:

- MYSQL_DATABASE=wordpress

volumes:

- dbdata:/var/lib/mysql wordpress:

image: wordpress:5-fpm-alpine

depends_on:

- db

container_name: wordpress

restart: unless-stopped

volumes:

- wordpress:/var/www/html

- /path/to/repo/myTheme/:/var/www/html/wp-content/themes/myTheme

env_file: .env

environment:

- WORDPRESS_DB_HOST=db:3306

- WORDPRESS_DB_USER=$MYSQL_USER

- WORDPRESS_DB_PASSWORD=$MYSQL_PASSWORD

- WORDPRESS_DB_NAME=wordpress webserver:

depends_on:

- wordpress

image: nginx:1.15.12-alpine

container_name: webserver

restart: unless-stopped

ports:

- "80:80"

- "443:443"

volumes:

- wordpress:/var/www/html

- /path/to/nginx-conf-folder/:/etc/nginx/conf.d

- certbotdata:/etc/letsencrypt

networks:

- app-network

depends_on:

- webserver

image: certbot/certbot

container_name: certbot

volumes:

- certbotdata:/etc/letsencrypt

- wordpress:/var/www/html

command: certonly --webroot --webroot-path=/var/www/html --email certbot:depends_on:- webserverimage: certbot/certbotcontainer_name: certbotvolumes:- certbotdata:/etc/letsencrypt- wordpress:/var/www/htmlcommand: certonly --webroot --webroot-path=/var/www/html --email youremail@email.com --agree-tos --no-eff-email --force-renewal -d domain.com -d www.domain.com volumes:

wordpress:

dbdata:

certbotdata:

Hope this works for you and you get things rolling!