WordPress 5 in Docker with Nginx and LetsEncrypt

TL;DR If you are comfortable with Docker and Docker Compose, you can go straight to the GitHub repo and get started.  For the everyone else, read on…

WordPress + Docker = <3

When I stood up this website, I wanted to do so in Docker, but I ran into an issue: the official WordPress Docker image runs Apache.  Apache is a nice webserver for small amounts of traffic, but it does not scale well.  As more concurrent connections come into a server running Apache, more copies of the httpd process are forked, which causes RAM usage to go up.  Having RAM usage regularly go up and down  is not ideal.

Fortunately, there is a better way.  The Nginx webserver, combined with PHP running in FPM mode scales much better as the memory usage is more constant, which means that peak loads on the server won’t cause you to thrash the swapfile.  Encryption would also be nice, so I wanted to have some SSL going as well.

I couldn’t find any existing solutions, so I built one!  In this post, I’m going to walk through each piece of the puzzle.

Docker Compose and MySQL

The very first thing we need to is create a file called docker-compose.yml.  This allows you to stand up multiple Docker conatiners and have them see each other on the same virtual network.  It handles things like DNS lookups (you can refer to containers by name), and makes sharing directories across multiple containers quite easy.

So open up your editor and put this into docker-compose.yml:

version: '3.3'

services:

   db:
     image: mysql:5.7
     restart: always
     volumes:
       - ./data:/var/lib/mysql
     environment:
       MYSQL_ROOT_PASSWORD: wordpressrootpw
       MYSQL_DATABASE: wordpress
       MYSQL_USER: wordpress
       MYSQL_PASSWORD: wordpress

There are two things of note in here: the volume, which lets our database persist outside of the container even if the container is stopped or destroyed, and the environment, which sets credentials in MySQL.  Environment variables are the normal way of passing settings into Docker containers.

You can now start that container by typing docker-compose up -d:

$ docker-compose up -d db
Starting wordpress-with-nginx-and-letsencrypt_db_1 ... done

Congrats, you have your first container running!  You can verify that it is running with docker-compose ps and see what it is writing to stdout with docker-compose logs:

$ docker-compose ps
                  Name                                Command             State          Ports       
-----------------------------------------------------------------------------------------------------
wordpress-with-nginx-and-letsencrypt_db_1   docker-entrypoint.sh mysqld   Up      3306/tcp, 33060/tcp

Note that while the container will show as “Up”, the underlying MySQL process may not be able to handle requests for 30 or more seconds, as the database will be initialized on the first run.  Assuming the data/ directory is left untouched, this will not be an issue when the container is started in the future.

PHP FPM and Nginx

The next piece of the puzzle is PHP FPM and Nginx.  We’ll treat these as a single unit since they are both responsible for serving up WordPress. 

Start by putting this into your docker-compose.yml file:

   php:
     image: wordpress:5-fpm
     depends_on:
       - db
     restart: always
     volumes:
       - ./php-uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
       - ./wordpress:/var/www/html
     environment:
       WORDPRESS_DB_HOST: db:3306
       WORDPRESS_DB_USER: wordpress
       WORDPRESS_DB_PASSWORD: wordpress

   web:
     image: nginx
     depends_on:
       - php
     restart: always
     volumes:
       - ./nginx.conf:/etc/nginx/conf.d/default.conf
       - ./wordpress:/var/www/html
       - ./logs:/var/log/nginx

We have a few new things for these two services:

  • We have a file called php-uploads.ini which is passed in.
  • We have a wordpress/ directory which will be populated when the php container is run for the first time.
  • We have a nginx.conf file which is passed into the web container
  • And finally, we have a logs/ directory which stores logs written by Nginx.

The php container runs PHP in FPM mode, which listens on port 9000 for requests from the webserver.  The module is pre-configured except for a few extra directives we’re going to put into php-uploads.ini, as follows:

file_uploads = On
memory_limit = 64M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 600

The above changes will allow files of up to 64 Megabytes to be uploaded to WordPress, which is important if you are dealing with large images or other media.

We also need to tell Nginx to send requests for PHP files to the php container.  That’s done by putting this into nginx.conf:

server {
    listen 80;

    root /var/www/html;
    index index.php;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    client_max_body_size 64M;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
    }
}

Note that we don’t have a server_name directive, since this instance of Nginx is only serving up one server.  That allows us to keep the configuration file simpler, and a simply configuration file is easier to understand and less likely to have configuration issues. 🙂 

Go ahead and start up those two containers with docker-compose up -d php web and you should see output like this:

$ docker-compose up -d php web
wordpress-with-nginx-and-letsencrypt_db_1 is up-to-date
Creating wordpress-with-nginx-and-letsencrypt_php_1 ... done
Creating wordpress-with-nginx-and-letsencrypt_web_1 ... done

The first time you run the above command, you may have more output as Docker images are downloaded for the first time.  It’s nothing to worry about, and is a one-time thing that Docker does. 🙂

HTTPS and Encryption

So at this point, WordPress is up and running and we could stop here.  But we really want to get HTTPS going, and there’s a very easy way to do that.  First, there is a service called Lets Encrypt which provides a service for creating SSL certs programmatically and second, there is a Docker container which allows us to further automate the process, as we’ll only need to configure that container.

Here’s the final piece of docker-compose.yml:

   https-portal:
     image: steveltn/https-portal:1
     depends_on:
       - web
     ports:
       - 80:80
       - 443:443
     restart: always
     volumes:
       - ./ssl_certs:/var/lib/https-portal
     environment:
       DOMAINS: 'localhost -> http://web:80 #local'
       CLIENT_MAX_BODY_SIZE: 64M

There’s a couple of interesting things in here.  The first one is the ports: section, which allows us to expose ports from Docker containers to the host system.  And while we’ve used volume: before, what it does this time around is let us keep the certificates that we’re creating between runs, as it is a CPU-intensive process.

We have an environment: section again, and this one is to tell https-portal what domains to listen for and what container to forward them to.  The #local part tells https-portal that we’re using a self-signed certificate.  If we were to run this on a server which is publicly accessible, we could replace that with #staging or #production to get an actual certificate from Lets Encrypt.

Finally, CLIENT_MAX_BODY_SIZE is a parameter which gets passed into this instance of Nginx.  The underlying scripts in that Docker container write their own nginx.conf which then has the client_max_body_size set to the value we supplied.

Go ahead and start this up with docker-compose up -d https-portal:

$ docker-compose up -d https-portal
wordpress-with-nginx-and-letsencrypt_db_1 is up-to-date
wordpress-with-nginx-and-letsencrypt_php_1 is up-to-date
wordpress-with-nginx-and-letsencrypt_web_1 is up-to-date
Creating wordpress-with-nginx-and-letsencrypt_https-portal_1 ... done

Putting it all together!

Go ahead and verify that all of the Docker containers are running:

$ docker-compose ps
                       Name                                     Command              State                    Ports                  
-------------------------------------------------------------------------------------------------------------------------------------
wordpress-with-nginx-and-letsencrypt_db_1             docker-entrypoint.sh mysqld    Up      3306/tcp, 33060/tcp                     
wordpress-with-nginx-and-letsencrypt_https-portal_1   /init                          Up      0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
wordpress-with-nginx-and-letsencrypt_php_1            docker-entrypoint.sh php-fpm   Up      9000/tcp                                
wordpress-with-nginx-and-letsencrypt_web_1            nginx -g daemon off;           Up      80/tcp

If that looks good, you should be able to go to http://localhost/, which will redirect you to https://localhost/ (with a self-signed certificate), and be shown the installation screen for WordPress.  Congrats!

All of the configuration above can be found on GitHub: https://github.com/dmuth/wordpress-with-nginx-and-letsencrypt

Got questions or comments?  Let me know in the comments.

— Doug