Setting up WordPress (php-fpm), Nginx and LetsEncrypt with docker compose


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


Leave a Reply

Your email address will not be published. Required fields are marked *