Installing the fastest WordPress stack with Ubuntu 18.04 and MySQL 8

If you’ve been following my stack guides, you’ll have seen how popular my previous stack-build guides were. That’s because they were incredibly fast for the price of the server you’re using. My previous guides included installing Varnish and PerconaDB. In this guide, these have both been replaced with new and better alternatives.

The stack includes:

  • Ubuntu 18.04 – the latest and greatest. Don’t go for 18.10 as it’s only supported for 9 months whereas 18.04 is supported until 2023.
  • MySQL 8 – the fastest MySQL ever released
  • Nginx – the fastest web server available
  • PHP 7.2 – the fastest PHP available
  • Redis – the fastest object and variant cache available
  • Nginx FastCGI Cache – the fastest HTTP accelerator available and it’s easy to use
  • Fail2ban
  • Letsencrypt

Hosting choices

You can choose whichever host you like, but I prefer Digital Ocean and not because I have an affiliate deal with them – it’s because they have 50s builds, they have great uptime, they have SSD disks, they have great prices and I’ve pretty much never experienced any issues with their servers.

Given that I said it’s not because of the affiliate deal, they do provide an affiliate deal but it’s a nice one that gets YOU guys some credit towards your next server. To take advantage, click this Digital Ocean Affiliate Link.

Otherwise, use whichever hosts you like, but this guide needs Ubuntu 18.04, so make sure you have that and the guide will work.

MySQL 8

I used to recommend PerconaDB, partly because they had the fastest database (comparable to MariaDB but 3x faster than MySQL 5.6), but more because they have a really great performance analysis toolkit.

Now, there is another tool you can use to analyse performance, and MySQL 8 has caught up performance-wise, so we’re back to the core track.

You might notice in the install script below that the installation installs the packages for 8.0.10 when 8.0.12 is the latest version, but don’t worry about that – because we’re adding the packages, apt-get update and apt-get upgrade then update us to the latest version.

Benchmarks

I have benchmarks coming shortly including comparisons of various stack options, comparisons of theme performance and comparisons of various plugins. There’s a scalability black-list and scalability white-list coming too, and for anything on the blacklist, I’ll have identified the specific performance problems suffered by these plugins, themes or shortcodes and hopefully the developers can fix these issues so they can get onto the whitelist.

Starting set of commands for the stack installation

The following is based on Ubuntu 18.04. Once you are logged in, run these commands one at a time. You can copy the line (triple-click the line to select the line) then paste into your server by right-clicking.

Unless otherwise stated, accept all the defaults or ‘y’ whenever asked.

wget -c https://dev.mysql.com/get/mysql-apt-config_0.8.10-1_all.deb
dpkg -i mysql-apt-config_0.8.10-1_all.deb
apt-get update
apt-get upgrade
apt-get install mysql-server -y # accept all defaults
mysql_secure_installation # choose y for everything and enter a secure root password
apt-get -y install php7.2
apt-get purge apache2
apt-get install nginx
apt-get install -y tmux curl wget php7.2-fpm php7.2-cli php7.2-curl php7.2-gd php7.2-intl 
apt-get install -y php7.2-mysql php7.2-mbstring php7.2-zip php7.2-xml unzip
apt-get install -y redis
apt-get install -y fail2ban

Now that everything is installed, we need to configure each item to be more optimal.

Configuring Redis to be a non-persistent cache

You don’t want Redis writing to disk – we’re just using it as an object-cache and variant-cache, and anything using the object-cache of variant-cache will survive the cache being wiped (it’ll just start building the cache again) so you need to alter the config to avoid disk writes which would otherwise slow your server down.

Edit /etc/redis/redis.conf and add the following 2 lines at the end:

vi /etc/redis/redis.conf #you can use nano to edit the file instead if you like, but I prefer vi

Scroll to the bottom of the file (just hit G in vi to get there), then hit ‘i’ to insert and insert the following lines:

#You can adjust this value as you see fit - 200mb or 20000mb
#it depends on how much RAM you have. On a 1GB server, I use 100mb.
maxmemory 3000mb
# this forces old keys to be deleted using first-in-first-out
maxmemory-policy allkeys-lru
The bottom of your file will end up looking something like this:

If you are using vi, hit ESC to exit ‘insert mode’. Now type /save (then press ENTER to exit search mode) to search for ‘save’. You can then hit ‘n’ to go to the next matching ‘save’ line until you find the lines below.
Comment out the three lines that start with save. Again, press ‘i’ to get to ‘insert mode’. Commenting out these 3 lines prevents Redis from writing anything to disk, giving you a true in-memory cache.
#save 900 1
#save 300 10
#save 60 10000
The lines will change colour (I think blue? maybe purple? I’m colour blind).

Once you have those lines commented out, hit ESC to exit insert mode then type :wq (and press ENTER) to write and quit vi. Now restart redis:
service redis-server restart
If Redis fails to start, double-check you have enough RAM and you didn’t ask Redis to use 3000mb on a 1000MB server, fix the file then use service redis-server start to get it going.

Configure DNS to point a domain name at your server

In order to support web traffic, you’ll need to point a domain name at your server. You can use a subdomain if you wish, e.g. dev.yourdomain.com. Regardless, login to your DNS provider and alter or create the A record to point traffic at your server’s IP address.
You may wish to create a CNAME record to point www.yourdomain.com at yourdomain.com. For example, here’s a suitable setup in Cloudflare:

Configuring Nginx to serve your website in the fastest possible way

I have uploaded some configuration files to github to make this step a lot easier. The configuration files will allow your site to be served over port 80 (non-SSL) in order to complete the initial WordPress installation. After that, you can configure SSL using either the Cloudflare flexible SSL or by using LetsEncrypt to have full end-to-end encryption.

The config files I use are built to allow processes to run for ages – this helps if you’re running massive import or export jobs etc – but you can modify them using the comments included in the config files.

cd ~
git clone https://github.com/dhilditch/wpintense-rocket-stack-ubuntu18-wordpress
cp wpintense-rocket-stack-ubuntu18-wordpress/nginx/* /etc/nginx/ -R
ln -s /etc/nginx/sites-available/rocketstack.conf /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/default

The files you cloned above and copied to your nginx folder include a config file for your website as well as various snippets to make your site fast and secure. The files use the nginx_fastcgi_cache library, and for that to work you need to create a cache folder.

mkdir /var/www/cache
chown www-data:www-data /var/www/cache/

To get these files into your nginx installation, you’ll need to restart nginx using the following command:

service nginx restart

Now your web server is ready for traffic, so visit www.yourdomain.com in your browser and check that you see the following:

The above confirms nginx loaded but the files it needs don’t exist yet.

Install WordPress

Before you install the WordPress files, you need to create a database. I tend to just use the command line like this:

mysql -u root -p

You’ll be asked for your MySQL password which you can paste using right-click.

Then run the following SQL, one line at a time after editing the 2nd command to use a strong password.

CREATE DATABASE rocketstack;
CREATE USER 'rs'@'localhost' IDENTIFIED BY 'CHOOSEASTRONGPASSWORD';
GRANT ALL PRIVILEGES ON rocketstack.* TO'rs'@'localhost';
EXIT;

You can install WordPress using the following set of commands:

wget https://wordpress.org/latest.zip -P /var/www/
unzip /var/www/latest.zip -d /var/www/
mv /var/www/wordpress /var/www/rocketstack
chown www-data:www-data /var/www/rocketstack -R
rm /var/www/latest.zip

Once done, if you reload your domain name you should see the WordPress installation screen. In the installation screen, you’ll be asked for the database name, the database username and the database password, so enter those from when you created the database and user.

In the example above, the database name is ‘rocketstack’, the username is ‘rs’ and the password is ‘CHOOSEASTRONGPASSWORD’. You can change these, and you should definitely change ‘CHOOSEASTRONGPASSWORD’, although with this config, and because we ran the secure mysql scripts, remote login to your MySQL server will be disallowed.

Once your WordPress installation is complete, you can move onto adding SSL to your site.

Adding SSL using Letsencrypt

I’ve made this pretty easy now because of the nginx scripts. Just run the following commands:

git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
mkdir /var/www/acme/
chown www-data:www-data /var/www/acme

The nginx script includes the necessary code to allow Letsencrypt certificate challenges. To actually obtain the certificate, you should run the following command – note: i’ve changed the formatting here so line wrap works, but copy the whole thing. Remember to edit yourdomain.com and www.yourdomain.com to reflect your own domain.

/opt/letsencrypt/letsencrypt-auto certonly -a webroot --webroot-path=/var/www/acme/ -d yourdomain.com -d www.yourdomain.com

Once it has completed, you’ll see something like the following – you need to copy the two file names so you can insert them into your nginx configuration file shortly. To copy text in putty/ssh, you just need to highlight it using your mouse. The shell will automatically copy anything highlighted into your clipboard, so you can then paste by using right-click.

Edit your nginx configuration file using:

vi /etc/nginx/sites-available/rocketstack.conf

You need to uncomment the SSL certificate lines and change the domain names to reflect your own domain name. Here are the two lines in the rocketstack.conf file:

Finally, to keep your SSL certificates active you should edit your crontab to renew certificates monthly:

crontab -e

Then go to the end of the file and add this line – note: again, I’ve changed the formatting here because the line is too long for all screens. Remember to enter your domain name twice again.

0 5 1 * * /opt/letsencrypt/letsencrypt-auto certonly -a webroot –webroot-path=/var/www/acme/ -d yourdomain.com -d www.yourdomain.com –renew-by-default

To test your SSL certificates, you should restart nginx and then visit your site but replace http with https. Restart nginx using:

service nginx restart

Changing your site to use SSL

For some weird reason, the WordPress installer fails miserably if your site starts out HTTPS. So, you have to install over HTTP and then convert to HTTPS. Now that your site serves up SSL traffic, you still need to make some alterations to be fully SSL.

Firstly, visit wp-admin -> Settings and change your WordPress Address and Site Address making both of them https instead of http.

If this is a brand new website, there’s only SSL redirects remaining – to send all traffic from http to https. You can do that with an nginx rewrite, or you can do it using Cloudflare.

You can test your pages, after you’ve changed your site address, and look for the padlock being broken. If it’s broken, you have insecure content being loaded on these pages.

Search/Replace SSL

If, instead of seeing https links above, you see https links, you need to fix these. One easy way to fix this is using a plugin but that’s the slowest possible way to fix it since full WordPress code needs to be loaded before the redirect kicks in.

Instead, you should use a search/replace plugin like Better Search and Replace or similar to replace all http://www.yourdomain.com references with https://www.yourdomain.com references.

If you have a massive site, you should probably use the Interconnectit script instead to search/replace in your DB and use sed to search/replace in your theme files.

https://interconnectit.com/products/search-and-replace-for-wordpress-databases/

Using SED to search/replace http with https in your WordPress theme files

It depends on how your theme developers have coded things. You ideally want a theme that uses //www.yourdomain.com as these type URLs are protocol agnostic, meaning they will use whatever protocol your pages were loaded over. But, many theme developers will have hardcoded http background images into any of your PHP, JS or CSS files so you need to find them and fix them to be https.

Logged into your server, using SSH, navigate to your wp-content/themes/child-theme folder.

cd /var/www/rocketstack/wp-content/themes/your-child-theme/

Now run something like the following line below. This will search and replace inside .php, .css and .js files so be careful. Take a backup of the folder prior to running your sed.

find ./ -type f -readable -writable -exec sed -i “s/http:\/\/www\.yourdomain\.com/https:\/\/www\.yourdomain\.com/g” {} \;

The command may look a little odd because with sed you need to escape some special characters, in our example the forward slashes and the dots.

Configuring Cloudflare SSL

If you configured Letsencrypt SSL, you can now configure FULL SSL so that traffic to your site is full encrypted. You *don’t* have to do this – if you are determined to lower your CPU usage, you can use the FLEXIBLE SSL option in Cloudflare, and this will mean traffic from your users to the Cloudflare servers is encrypted, but traffic from Cloudflare to your server is NOT encrypted.

I personally prefer to have the traffic encrypted all the way through, and I think you have an obligation to do so – you do not know who is packet sniffing on routers between Cloudflare and your own servers.

Anyway – it’s easy to enable FULL SLL – just log in to Cloudflare, hit the Crypto menu button and choose the FULL dropdown option.

To test that SSL is enabled all the way through, you can run the command below. The nginx configuration files save access logs to two different locations, depending on whether it’s encrypted or not.

View the latest encrypted traffic access logs:

tail /var/log/nginx/rocketstack_ssl_access.log

View the latest unencrypted traffic access logs:

tail /var/log/nginx/rocketstack_access.log

If you’d like to force all traffic through SSL, I recommend you create a page rule using Cloudflare. It’s the easiest way.

If you’re using Cloudflare, you should install and configure the Cloudflare plugin.

Redirecting all traffic to https

You can either use a Cloudflare page rule, or you can use an nginx rewrite rule. The cloudflare page rule eliminates some work from your server, in the cases where traffic is trying to visit old http links, so that’s the preferred option.

Log into Cloudflare and create a new page rule. Enter your http://www.yourdomain.com domain name and choose ‘Always HTTPS’. Save and deploy.

You should already have your WordPress site URL set to https://www.yourdomain.com/ so https://yourdomain.com traffic should already be getting redirected to the correct URL. Here’s an example page rule in Cloudflare for HTTPS.

Optimising your MySQL configuration

The mysql configuration files you need to edit are a little different to the previous PerconaDB installations I used to use.

In this stack guide, I’m recommending that you initially modify your mysqld (MySQL daemon) configuration file as follows, then run your site for a while and once you have typical traffic for a while, you should run the performance optimisation script further down this article. That performance script will help you configure your MySQL configuration for your particular server abilities and traffic behaviour.

Firstly, edit your mysqld.cnf file:

vi /etc/mysql/mysql.conf.d/mysqld.cnf

To start with, just add these basic optimisations at the end of the file:

innodb_buffer_pool_size = 200M
innodb_buffer_pool_instances = 8
innodb_io_capacity = 5000
max_binlog_size = 100M
expire_logs_days = 3

Once you have run your new server for a while, with real traffic, follow these instructions to optimise further.

Optimising your MySQL configuration after you’ve run traffic for a while

Once you have traffic running for a day or two, you should download the tuning primer script. It’ll inform you of any modifications you should make to your mysqld.cnf file. Here’s how to install and run it:

cd ~
wget https://launchpadlibrarian.net/78745738/tuning-primer.sh
./tuning-primer.sh

It outputs information and colour codes red or green for ‘needs work’ or ‘fine’. Once you have some decent traffic, run the primer and follow the instructions to optimise your MySQL configuration further.

Optimising your PHP configuration for WordPress

By default, your PHP configuration will probably not be good enough for you. This section will tell you the areas you need to look at, but the configuration you choose depends on your traffic and the amount of RAM you have.

The first file to edit to optimise PHP is the php.ini file.

vi /etc/php/7.2/fpm/php.ini

You should take your time to scroll down through this file and figure out if there’s anything else you’d like to modify, but the two key entries you should change are:

max_execution_time = 6000
memory_limit = 512M
upload_max_filesize = 50M

The defaults for the above are 30s, 128M and 2M which are not enough for modern websites. So, edit the php.ini file, find these lines, change whatever else you see needs altering and then restart PHP. Before you restart PHP, you should also edit your www.conf file.

The other part of PHP configuration you need to alter is the www.conf file. This controls how many simultaneous PHP processes will be spawned. For best performance, you should configure this to have all the processes already spawned so that when traffic builds, the processes are already available to server traffic.

vi /etc/php/7.2/fpm/pool.d/www.conf

The choices you can see in the comments section, but what you want for best performance is:

pm = static

The default is pm = dynamic. If you set pm = static, you can then set pm.max_children to control how many simultaneous PHP processes will be running the entire time your server is running.

Once you have altered and saved this file, you can now restart the PHP service.

service php7.2-fpm restart

Configuring fail2ban to eliminate bot traffic before it ever hits WordPress

If you’ve ever thought about using WordFence or Sucuri, they’re not bad plugins. The problem is that loading WordFence or Sucuri involved a whole bunch of expensive elements of your WordPress stack including Cloudflare, Nginx, PHP and MySQL. If you can stop the traffic earlier, it will only involve Cloudflare or Nginx.

In order to stop traffic at the Cloudflare level costs some money – they provide a web firewall, but for performance reasons this would be your most performant option.

If you can’t afford that, or if you reject paying money for something you can sort out for time rather than money, you can configure fail2ban.

The basic install, if you’ve followed the installation above, automatically includes SSH/putty attacks and blocks those attacks based on IP addresses.

I will write a separate article about configuring fail2ban as it can be complicated, but if you wish to get this set up, you should install the WP fail2ban plugin and follow their guide for adding their ‘jails’ and ‘filters’. Basically, fail2ban uses filter config files to spot dodgy traffic and then uses the jail config files to decide how long to ban them.

Configuring Redis for object caching and transient caching

Many plugins will use transients to store information that helps speed up their plugin operations. If you do not have an object cache enabled, these variants will be stored in your MySQL database. That is not ideal, since that involves writing to disk. Even SSD disk operations are slow compared to RAM operations. At the top end of RAM speeds you’re talking 20GB per second versus SSD top-ends of 200MB per second. So, you want to make sure your transients are stored in RAM, as well as your objects stored in your cache.

You’ve already configured Redis to store in RAM, so all you need to do now is install the correct plugin.

The one you want to install is called the Redis Object Cache, by Till Kruss, and *not* the WP Redis plugin.

Installation is simple, install the plugin, activate it, then visit Settings->Redis and click ‘Enable Object Cache’. You’ll then see the Status: Connected.

If you have Query Monitor installed, you’ll notice now that the number of queries running per page is massively reduced. The reason the object cache helps is two-fold – firstly, the MySQL queries do not need to run again, but also the PHP that runs and processes the results of the MySQL queries and creates an object doesn’t need to run again.

This is entirely safe – it speeds up your site massively, both through the object cache and through the storage of transients in the Redis memory cache.

What about page caching?

You do not need a page caching plugin with the above stack because the nginx fastcgi cache handles that and stores pages cached under your /var/www/cache folder. That is faster for your site, because the full HTML is cached up for users using nginx only, before PHP or MySQL is touched or invoked.

There is a plugin you can install if you need to flush your nginx cache on-demand, and/or when new articles are released. The plugin is also by Till Kruss and is called the Nginx Cache plugin.

Summary

Follow the guide above to get the most bang possible for your buck. I prefer to use Digital Ocean droplets with their fast SSD disks, cheap prices and 50 second setup speeds.

But getting fast disks is not enough – you need to make sure the software you install is the most performant available. Nginx uses less memory compared to Apache, and is faster, so both per-page performance and simultaneous user-capacity is improved.

I’ve made the guide above as simple as possible for anyone to follow, but please ask questions below because I can’t predict everything you might ask. I love questions, so ask away and I’ll flesh out this guide to cover anything I may have missed.