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…
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