Deploying Wordpress with docker + SSL

Deploying wordpress using docker helps in isolation specially when deploying it on a shared server.

For dedicated servers it is also good to use docker for deploying wordpress since it helps us manage our wordpress installation and in the future can just swap it out for something else if needed.

You can also checkout the template I created for this here.


Prerequisites


  1. A server with a static public IP
  2. A Domain name already pointed to the server's static public IP
  3. docker engine
  4. docker-compose
  5. git

docker-compose.yml


First lets create the docker-compose.yml contents.


services:

    wordpress:
        container_name: wordpress
        depends_on:
        - db
        image: wordpress:latest
        restart: unless-stopped
        volumes:
        - './wordpress:/var/www/html:z'
        environment:
        WORDPRESS_DB_HOST: db
        WORDPRESS_DB_NAME: wordpress
        WORDPRESS_DB_USER: wordpress
        WORDPRESS_DB_PASSWORD: password

    container_name: db
        image: mariadb:lts-noble
        volumes:
        - './.db/data:/var/lib/mysql'
        restart: unless-stopped
        environment:
        MYSQL_ROOT_PASSWORD: root
        MYSQL_DATABASE: wordpress
        MYSQL_USER: wordpress
        MYSQL_PASSWORD: password

The above is minimum requirements needed for wordpress deployment.


If you are using cloudflare with proxy you can just expose port 80 just like below and let cloudflare handle the SSL proxy.


    wordpress:
        ...
        port:
            - '80:80'
        ...

If you are not using cloudflare or you want to just handle your own SSL certificates then continue reading.


To handle our own certificates we need to expose our wordpress using a reverse proxy like nginx and configure a https to http proxy pass, lets add nginx to our docker-compose.yml.


services:

    nginx:
        container_name: nginx
        image: nginx:latest
        profiles:
        - prod
        ports:
        - '80:80'
        - '443:443'
        restart: unless-stopped
        volumes:
        - './nginx/conf/:/etc/nginx/conf.d/:ro'

    wordpress:
    ...

Now since we need our SSL certs we can use Certbot to create it for us. Let's add it to our docker-compose.yml


services:

    nginx:
        ...

    certbot:
        container_name: certbot
        image: certbot/certbot:latest
        profiles:
        - prod
        volumes:
        - './certbot/www/:/var/www/certbot/:rw'
        - './certbot/ssl/:/etc/letsencrypt/:rw'

    wordpress:
    ...

How certbot normally works is that it reads the nginx configs using --nginx flag, but since nginx and certbot are on different containers and certbot cannot check the nginx configs (unless we do some magic configuration or build our own docker images which is not the goal here) we can use the --webroot flag.


What the --webroot flag does is that it creates some verification files that will be deployed by nginx specifcally in the directory /.well-known/acme-challenge/, this is then read by certbot using http://<your domain>/.well-known/acme-challenge/ to verify the domain and create the SSL certificates.


verifcation/challenge files will be created inside /var/www/certbot/ inside the docker container so we mount that to our local directory in ./certbot/www/, SSL certificates are also generated in /etc/letsencrypt/ so we also mount our local directory ./certbot/ssl/ this is so that we can pass it to our Nginx container by also mounting those specific directories.


services:

    nginx:
        container_name: nginx
        image: nginx:latest
        profiles:
        - prod
        ports:
        - '80:80'
        - '443:443'
        restart: unless-stopped
        volumes:
        - './nginx/conf/:/etc/nginx/conf.d/:ro'
        - './certbot/www:/var/www/certbot/:ro' # mount this so we can deploy the certbot verifcations
        - './certbot/ssl/:/etc/nginx/ssl/:ro' # mount this for the SSL

    certbot:
    ...

Generating the SSL certs


We should then need to create the nginx conf files and put it in ./nginx/conf/. Note: name it what ever you want as long as it has the .conf extension.


server {
    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

What the above does is that it deploys the certbot verification files in /.well-known/acme-challenge/ using http. Other than that if it will redirect to use https connection.


Before continuing with adding our https to http proxy pass we should first generate the SSL certificates. What we have done so far is create a way for our certbot to generate the SSL certs. Note: if you skipped this part then you first need to disable your https to http proxy pass since it will fail your nginx and will prevent you from creating your SSL certificates.


Run first your docker-compose to up your containers and enable certbot to generate the SSL files.

services:

    nginx:
        container_name: nginx
        image: nginx:latest
        profiles:
        - prod
        ports:
        - '80:80'
        - '443:443'
        restart: unless-stopped
        volumes:
        - './nginx/conf/:/etc/nginx/conf.d/:ro'
        - './certbot/www:/var/www/certbot/:ro' # mount this so we can deploy the certbot verifcations
        - './certbot/ssl/:/etc/nginx/ssl/:ro' # mount this for the SSL

    certbot:
    ...

We should then need to create the nginx conf files and put it in ./nginx/conf/. Note: name it what ever you want as long as it has the .conf extension.


sudo docker-compose up -d

Wait for a few seconds to finish and then run the command below to invoke certbot's SSL creation process.


sudo docker-compose run --rm  certbot certonly --webroot --webroot-path /var/www/certbot/ -d <domain here> --dry-run

Once fullfilling the inputs it should show successful just like below.


Saving debug log to /var/log/letsencrypt/letsencrypt.log
Simulating renewal of an existing certificate for <domain name>
The dry run was successful.

The above uses the --dry-run flag to verify if certbot is able to generate the SSL certificates. If you encounter an error make sure the domain name is correct and your domain is already pointing to your server's public IP.


Once satisfied with the above you can then run the above command again and this time without the --dry-run flag.


sudo docker-compose run --rm  certbot certonly --webroot --webroot-path /var/www/certbot/ -d <domain here>

The we should now have our SSL certificates.



Deploying or HTTPS to HTTP proxy pass


Now that we have our SSL certs we should now be able to create our nginx conf for https to http proxy pass.

server {
    listen 443 ssl;
    listen [::]:443 ssl;

    server_name <domain here> www.<domain here>;
    server_tokens off;

    ssl_certificate /etc/nginx/ssl/live/<domain here>/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/<domain here>/privkey.pem;

    location / {
        proxy_pass http://wordpress:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Add the above just below your previously created .conf or create another .conf file and add it there.


With our previous .conf it should redirect any http connection other than the one used by certbot to use https (port 443).


What the above .conf file does is that it listens for any https connection and proxy pass it to our wordpress container essentially exposing it with an upfront SSL connection.


We should now be able to access our domain via browser and see the wordpress setup/installation page.


Summary


So we were able to deploy our wordpress using docker and expose it using nginx with SSL/https. We were also able to use certbot to create our SSL certificates but since these certificates expire within 3 months we can also leverage certbot to renew it using the command below.


sudo docker-compose run --rm certbot renew

Together with crontab we can automate it to run every 2 months (crontab has some interval limitations, to prevent the cert from expiring first we can just run renew every 2 months).


Below is the crontab command I use to run it every 2 months.


0 0 1 */2 * cd /opt/talents.is && sudo docker-compose run --rm certbot renew

Be mindful of what day/time you created the cert, you may want a custom cron interval, the above cronjob runs the command every 2 months starting from the 1st of January.


Also take a look at the template I created for a complete solution and ready to use docker-compose.yml.