I’ve finally decided to start blogging and since I like to tinker I decided to self-host WordPress on DigitalOcean. I chose DigitalOcean because it has very cost effective instances (starting at $4/mo). I decided to use docker compose to deploy because of its simplicity. Since this won’t be a very heavily used site, I’m not anticipating having to scale horizontally so docker compose was a perfect fit. Enough talk, let’s dig in!
Step 1: Nginx and LetsEncrypt
The goal of this step is to minimally setup nginx and certbot so that we can issue a LetsEncrypt certificate. The requirements are a valid domain with A records setup for both the root domain (tomescu.ca) and www subdomain (www.tomescu.ca). You can substitute your own domain wherever you see tomescu.ca.
First thing that we want to do is setup two services, nginx and certbot:
docker-compose.yml
services:
nginx:
image: nginx:1.25.3-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
certbot:
image: certbot/certbot:v2.7.4
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
The main takeaway here are the volume mounts. Lets take a look at the certbot mounts:
- ./certbot/conf:/etc/letsencrypt – this is where the LetsEncrypt certificates will be stored
- ./certbot/www:/var/www/certbot – this is where the LetsEncrypt domain verification files will be stored (note: they will only exist temporarily until the certificate is issued and will be removed after – I was originally confused as to why this directory is empty)
These two directories must also be mounted in the nginx container so that nginx can access the certificates and verification files. We’re also mounting another directory ./nginx/conf:/etc/nginx/conf.d:ro where we’ll store the nginx config.
Next, we need to create a new nginx config:
nginx/conf/wordpress.conf
server {
listen 80;
listen [::]:80;
server_name tomescu.ca www.tomescu.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
This is not the full nginx config, just a minimal version required to obtain the first LetsEncrypt certificate. The first location block serves the certbot verification files while the second location block redirects all other requests to the https server. Note that we haven’t configured the https server yet – we need to obtain a certificate first.
Now we’re ready to issue a LetsEncrypt certificate. Start up nginx (docker compose up -d) and request a certificate (optional: test first with –dry-run):
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ -d tomescu.ca -d www.tomescu.ca
Success – now you should see your LetsEncrypt data in the certbot/conf directory.
Last thing that we want to do is automate the certificate renewal process. Edit the services to override nginx’s command and certbot’s entrypoint:
docker-compose.yml
services:
nginx:
image: nginx:1.25.3-alpine
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
certbot:
image: certbot/certbot:v2.7.4
restart: unless-stopped
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
Now nginx will reload its config every 6 hours and certbot will renew the certificate every 12 hours. Run docker compose up -d
to start the services.
Step 2: WordPress
The goal of this step is setting up WordPress php-fpm.
We need to add two new services, wordpress and db, as well as add a volume mount to the nginx service:
docker-compose.yml
services:
nginx:
image: nginx:1.25.3-alpine
command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
- wordpress:/var/www/html
certbot:
image: certbot/certbot:v2.7.4
restart: unless-stopped
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
wordpress:
image: wordpress:php8.2-fpm
restart: unless-stopped
depends_on:
- db
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: secure-password
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress:/var/www/html
db:
image: mysql:8.2.0
command: [ "--max_connections=1000" ]
restart: unless-stopped
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: secure-password
MYSQL_RANDOM_ROOT_PASSWORD: "yes"
volumes:
- db:/var/lib/mysql
volumes:
wordpress:
db:
Notice how we mounted the wordpress volume in both the wordpress service and nginx service using the same path. This is required in order to properly reverse proxy to the php-fpm service.
Next, we need to update our nginx config:
nginx/conf/wordpress.conf
server {
listen 80;
listen [::]:80;
server_name tomescu.ca www.tomescu.ca;
server_tokens off;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 default_server ssl http2;
listen [::]:443 ssl http2;
index index.php;
root /var/www/html;
server_name tomescu.ca;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
ssl_certificate /etc/letsencrypt/live/tomescu.ca/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tomescu.ca/privkey.pem;
location / {
try_files $uri $uri/ /index.php?$query_string;
gzip_static on;
}
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;
}
}
Key takeaways:
- Directive root /var/www/html; must match the nginx services’ wordpress volume mount
- Directive ssl_certificate /etc/letsencrypt/live/tomescu.ca/fullchain.pem; and ssl_certificate_key /etc/letsencrypt/live/tomescu.ca/privkey.pem; point to the LetsEncrypt volume mount
- Directive fastcgi_pass wordpress:9000; points to the wordpress php-fpm service
Step 3: We’re done!
Now that we have WordPress configured, we can go ahead and start everything up (docker-compose up -d). Congrats!
Resources
https://github.com/dbtek/docker-compose-wordpress-fpm-nginx
https://mindsers.blog/post/https-using-nginx-certbot-docker/
https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71