Hosting Wordpress in Docker with SSL 2021

Zachary Tyhacz
8 min readOct 23, 2020
hosting wordpress in docker is easy bro

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;
# 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, make sure you have a couple things setup first:

  • You have purchased a public domain on a DNS service.
  • Your domain has proper name-servers setup
  • Your domain points to this server you are setting up Wordpress + Docker on. ( an A record pointing to your server’s ip address )

If all of that, you’re good!

We need to add Certbot as a service to our docker-compose then do 3 steps:

  1. While Nginx is running the HTTP config, we run a Certbot command to get certificate.
  2. We swap the Nginx configuration with an HTTPS config
  3. 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 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;
}
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 :

# 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
certbot:
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 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!

--

--