If you’ve ever dealt with WordPress, you’ve probably noticed that it’s not the fastest software out of the box. This is not inherently a problem with WordPress, as it is a complex app that runs on a cross platform technology. Performance of WordPress though can be improved with the combined effects of several small tweaks.
This guide will walk through a recipe for setting up WordPress with Docker containers an tuning those containers to maximize the performance of WordPress. For the sake of simplicity, it’s assumed that you’re at least familiar with WordPress and Docker, and you have a machine with Docker already running on it. If not, there are many ways you can get Docker. For a guide to migrating content to Docker, check out this post here.
One of the fundamentals though for running a performant WordPress install is to have a good host. I use Microsoft Azure for my websites, and they are rather speedy. The machine itself doesn’t have to be beefy. A single core box can handle sites that get thousands of hits per day without a problem, and they don’t cost a fortune either.
The basic architecture for this recipe is to use three containers to host the site.
- NGINX serves multiple roles in this setup. First, it is responsible for serving up static content – that is all of the stuff that is just files on the the site such as images, JavaScript files, CSS files and so on. Second, NGINX dynamically compresses outgoing streams of data from WordPress. Dynamic compression is useful for reducing the size of the payload that are placed on the wire, meaning your site gets downloaded faster. Thirdly, NGINX acts as a reverse proxy for the PHP content. And Lastly, NGINX will inject headers for the static content to take advantage of browser caching.
- PHP FPM is responsible for handling all things PHP. Historically, PHP was mostly handled by the web server itself. Each request was a process and thread, which made the PHP execution inefficient. PHP FPM came along and optimized threading (and other things!) for PHP applications, which helped improve the performance of PHP apps in general.
- Lastly, this setup uses MySQL, which is the database.
In addition to the performance gains from NGINX and PHP FPM turning on WP Total Cache will give an extra boost in performance too.
Prepare the Host Server
With any container, it’s always a good idea to store persistent data outside of the container. This is basically anything that you can’t stand to lose. In the context of WordPress, this would be your configuration files, custom themes, images and other media, and the database.
- Create a Docker Network for WordPress
docker network create wp-net
- On your Docker host, create a folder on the file system called docker and cd to that folder.
mkdir /docker && cd /docker
- In the Docker file, create a folder called www and cd to that folder.
mkdir www && cd www
- For a fresh install of WordPress, download WordPress and extract it here.
wget https://wordpress.org/latest.zip && unzip latest.zip
Then, rename the wordpress folder html.
mv wordpress html
- For a content migration, create an html folder and then move all your existing content into the folder. Moving the content will requires you to download it from your existing host and move it to your new host. It’s best to compress all the content into a single zip file or tar.gz file to transfer the content.
mkdir html
MySQL setup
MySQL is pretty easy to setup. We’ll setup a folder on the host machine and mount that folder as a volume in the Docker container.
- Create a folder in the docker folder called database.
mkdir database
- Now, run the container. You’ll notice that the newly created database folder is mounted as a Docker volume in the container. Change the password to a password of your choosing.
docker run --name mysql --restart always -v /docker/database:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=yourpassword --net wp-net -d mysql
- If you’re migrating content, you’ll need to import the database dump from your previous install. You can do this by first creating the database. The name of the database should probably be the name of the database you exported, so replace wpdb with that name. Don’t forget to use the correct password.
docker exec -i wp-mysql mysql -u root --password=yourpassword <<< "CREATE DATABASE wpdb;"
Then, import the data. Again, don’t forget to use the correct password. Note: the path to the .sql file from the dump is on the host computer, not a path in the container.
docker exec -i wp-mysql mysql -u root --password=yourpassword wintellectcomwp < /path/to/your/wp.sql
PHP-FPM setup
Setting up the PHP-FPM container is pretty straightforward. It just requires mapping the www folder you already created earlier to the appropriate place in the container so that PHP-FPM can find the php file. This particular container is using PHP 7 with FPM which performs better than previous versions of PHP such as 5.6 or 5.7.
docker run -dit --name fpm --restart always --net wp-net -v /docker/www:/var/www/ wordpress:4.8-php7.1-fpm
NGINX Setup
The NGINX setup is probably the most complicated because it requires setting up two configuration files and mounting the www folder and these two files in the container.
- Create a folder called nginx in the docker folder and cd to the new folder.
mkdir nginx && cd nginx
- Create an NGINX configuration file called nginx.conf. Copy and paste the following text into the file. This is a pretty generic nginx.conf file setup to enable dynamic content compression. Probably the only thing you’ll need to modify is the worker_processes. Set it to the number of CPU cores on the host server.
user root; # Set this to the number of CPU cores available to your computer. worker_processes 2; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; gzip on; gzip_disable "msie6"; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_buffers 16 8k; gzip_http_version 1.1; gzip_min_length 256; gzip_types text/html text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon; include /etc/nginx/conf.d/*.conf; }
- Now, create another file called default.conf from the following text. Again, this is a pretty generic configuration file for NGINX. This file sets up the site specific configuration for the site. It contains the directives for setting up browser caching as well as the paths for static content and the reverse proxy for PHP FPM. Change server_name to the name of your domain. Save the file.
map $sent_http_content_type $expires { default off; text/html epoch; text/css max; application/javascript max; ~image/ max; } server { listen 80; server_name www.yourdomain.com; root /var/www/html; index index.php; location / { try_files $uri $uri/ /index.php?$args; } location ~ .php$ { try_files $uri =404; fastcgi_split_path_info ^(.+.php)(/.+)$; fastcgi_pass fpm:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } location ~* .(js|css|png|jpg|jpeg|gif|ico)$ { expires max; } }
- With the configuration done, you can now create the NGINX container. The following command maps the two configuration files to the appropriate places in the container as Docker volumes. Likewise, it also exposes port 80 to the external network to receive traffic.
docker run --name nginx -dit --restart always --net wp-net -v /docker/nginx/nginx.conf:/etc/nginx/nginx.conf -v /docker/nginx/default.conf:/etc/nginx/conf.d/default.conf -v /docker/www:/var/www/ -p 80:80 nginx
Final steps
Now, all the containers are running, but there’s a few random things left to do.
- Change ownership of the files in the www folder. You’ll need to set the owner to www-data.
chown -R www-data:www-data /docker/www/*
- If you’re migrating content, be sure to edit the wp-config.php found in /docker/www/html to match the new settings for your WordPress install. The DB_NAME will be whatever the database you created for the import when you set up MySQL. The DB_USER can be root, the DB_PASSWORD is whatever you used when you created the database container, and lastly the DB_HOST is mysql, the name of the database container itself.
/** MySQL database */ define('DB_NAME', 'wpdb'); /** MySQL database username */ define('DB_USER', 'root'); /** MySQL database password */ define('DB_PASSWORD', 'yourpassword'); /** MySQL hostname */ define('DB_HOST', 'mysql');
- If you’re setting up a new site, point your browser to the website and run the MySQL setup wizard. Use mysql for the database host, root for the database username, and the password you set whenever you created the database container. You can use whatever database name you like.
- After you have WordPress installed or migrated, install the W3 Total Cache plug-in and activate it. In the settings for the plug-in, turn on Page Cache, Opcode Cache, Database Cache, Object Cache, and Browser Cache
Final Thoughts
I’ve used this recipe on a number of sites and all of them load rather quickly. You can verify the results of your website by looking at performance metrics on www.webpagetest.org Most of my sites get “Straight A’s” with this recipe.
Also, while Docker isn’t required, it does make setting up many of these software packages easier because they are already preconfigured. The same principles can be applied to a VM for optimal performance too.