Installing and configuring the fastest possible WordPress stack on Ubuntu 16.04

This article presumes you have a fresh Ubuntu 16.04 server and you can log into it. I recommend using SSH keys if you can, but that doesn’t affect this article.

Install PerconaDB

Run the following commands to install PerconaDB:

wget https://repo.percona.com/apt/percona-release_0.1-4.$(lsb_release -sc)_all.deb
dpkg -i percona-release_0.1-4.$(lsb_release -sc)_all.deb
apt-get update
apt-get upgrade
apt-get install percona-server-server-5.7 -q -y

Note: The Percona installation will ask you for a root password. This is the root password for your database and nothing to do with the root password for your server. If you wish, leave the password blank and it will still be secure because remote logins will be disabled.

Install Nginx and PHP 7

Run the following to install Nginx and PHP 7:

apt-get install nginx -y
apt-get install php -y
apt-get install -y tmux curl wget nginx php7.0-fpm php7.0-cli php7.0-curl php7.0-gd php7.0-intl 
apt-get install -y php7.0-mysql php-memcached php7.0-mbstring php7.0-zip php7.0-xml

# now we need to temporarily set up nginx to work over HTTP so we can install WordPress and create SSL certificates
git clone https://github.com/dhilditch/wordpress-rocket-stack /root/wordpress-rocket-stack/
cp /root/wordpress-rocket-stack/etc/nginx/* -R /etc/nginx/
rm /etc/nginx/sites-enabled/default.conf
ln -s /etc/nginx/sites-available/install.wordpress.rocketstack.conf /etc/nginx/sites-enabled/

wget https://wordpress.org/latest.zip -P /var/www/
apt-get install unzip
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
#if we are on a single CPU host, run the following
mkdir /etc/systemd/system/nginx.service.d
printf "[Service]\nExecStartPost=/bin/sleep 0.1\n" > /etc/systemd/system/nginx.service.d/override.conf
systemctl daemon-reload

service nginx restart

Ok – so now point your domain name at the IP address of your server, and when you visit you should see the WordPress installation screen.

We need a database, and we also would like SSL for *after* WordPress installation – unfortunately, installing WordPress using SSL is buggy, but it’s easy to switch AFTER installation.

Connect to mysql using:

mysql

Or if you entered a password for root, you’ll need to connect using this:

mysql -u root -P

Then run the following SQL:

create database rocketstack;
grant all privileges on rocketstack.* to [email protected] identified by 'CHOOSEASTRONGPASSWORD';

Visit your domain URL through a browser and you’ll see the WordPress installation screen. Choose your language, then the 2nd screen will ask you for the database name, database user name, and database password – use the strong password you used in the last line.

Once you have entered those details, continue through to finish your WordPress installation. Once done, we will install SSL and the rest of the components.

Install SSL using Letsencrypt

Letsencrypt gives you free SSL certificates and I’ve simplified the scripting of this for you.

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

Edit /etc/nginx/snippets/acme-challenge.conf and replace yourdomain.com with your actual domain – this is on one line, and that line looks like this:

 return 307 $scheme://acme.yourdomain.com$request_uri?redirect=yes;

So, if for example you have the domain wpintense.com, it would become:

return 307 $scheme://acme.wpintense.com$request_uri?redirect=yes;
Once done, restart nginx and then run letsencrypt:
service nginx restart
/opt/letsencrypt/letsencrypt-auto certonly -a webroot --webroot-path=/var/www/acme/ -d yourdomain.com -d www.yourdomain.com --agree-tos
It will ask for your email address which is useful, but below we set up auto renewal.
Edit your cron jobs with:
crontab -e
And add a line like the following, but with your domain(s) instead:
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

Now, we want to disable the temporary nginx conf file used and replace them with the production settings.

Edit the file /etc/nginx/sites-available/varnish.rocketstack.conf and replace the 2 SSL lines with the correct location of your newly generated SSL files – this involves simply replacing yourdomain.com on these 2 lines:

ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

Once you have edited that file, we will remove the temporary nginx config and enable production config.

ln -s /etc/nginx/sites-available/wordpress.rocketstack.conf /etc/nginx/sites-enabled/
ln -s /etc/nginx/sites-available/varnish.rocketstack.conf /etc/nginx/sites-enabled/
rm /etc/nginx/sites-enabled/install.wordpress.rocketstack.conf 
service nginx restart
If you refresh your WordPress site in your browser, it should automatically redirect to https now.
So far, we have PerconaDB, PHP 7 and Nginx configured. But Varnish and Redis are not yet installed.

Install and configure Varnish

apt-get install varnish
cp /root/wordpress-rocket-stack/etc/varnish/default.vcl /etc/varnish/default.vcl
service varnish restart
That’s that – the default.vcl is configured to work properly with WooCommerce and WordPress comments as well as other popular plugins.

Installing Redis

apt-get install redis-server
apt-get install php-redis
Edit /etc/redis/redis.conf and append the following 2 lines:
maxmemory 128mb
maxmemory-policy allkeys-lru
You can alter the maxmemory how you wish for the RAM you have available. Restart Redis to use the updated config with:
service redis-server restart
service php7.0-fpm restart

Optimising PerconaDB Config

Edit the following file /etc/mysql/my.cnf and replace/add the following config:
[mysqld]
key_buffer = 512M
max_allowed_packet = 512M
thread_stack = 192K
thread_cache_size = 8
query_cache_limit = 512M
query_cache_size = 4192M #(or 0 can improve performance, it depends)
innodb_buffer_pool_size = 20000M #(according to your memory - as a guide, use 60% of your memory here)
innodb_buffer_pool_instances = 16 #(according to how many cores you have)
innodb_io_capacity = 5000 #(because Digital Ocean use fast SSDs so we don't want InnoDB to be limited)
Adjust the above according to how much RAM you have. The above is based on a 30GB server, so if you have 4GB, divide all the M values by 10, e.g.
[mysqld]
key_buffer = 51M
 max_allowed_packet = 51M
 thread_stack = 19K
 thread_cache_size = 8
 query_cache_limit = 51M
 query_cache_size = 419M
 innodb_buffer_pool_size = 2000M
 innodb_buffer_pool_instances = 16
 innodb_io_capacity = 5000

Installing and configuring fail2ban

Fail2ban is a great tool for protecting your server. By banning bots through the firewall you get far faster performance than using plugins like WordFence.
sudo apt-get install fail2ban
cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Create /etc/fail2ban/filter.d/wordpress.conf and add the following lines:

[Definition]
failregex = ^.*POST.*(wp-login\.php|xmlrpc\.php).* (403|499)

Then edit /etc/fail2ban/jail.conf:

Change bantime at top to 86400 (1 day instead of 10 minutes)

Find [nginx-http-auth] and add below it:

enabled=true

So, it should look something like this:

[nginx-http-auth]

enabled = true
filter = nginx-http-auth
port = http,https
logpath = /var/log/nginx/error.log

Add the following after the nginx section in the jail.conf:

[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = /var/log/nginx/*access.log
maxretry = 10

Then restart fail2ban with:

service fail2ban restart

Installing and configuring W3 Total Cache

I’ve tried all of the caching plugins, but the free version of W3 Total Cache is the best – just be sure to avoid using it to compress files (we already compress through Nginx) and don’t use their minification (if you want minification, use Cloudflare instead).

Here are the settings I tend to use:

Page caching: Disk Enhanced

Object caching: Redis

Browser Caching: Yes, then modify browser caching settings to enable adding the expires header and all other headers, but untick ‘compress’ files.

Reverse proxy: Tick the checkbox and enter 127.0.0.1 in the textarea so your local varnish cache can be purged.

Cloudflare Configuration

The above configuration files include configuration to pass through the correct IP addresses from your visitors, even when using Cloudflare, meaning fail2ban will work properly, but there are still some Cloudflare settings to edit:

Caching -> Browser expiration -> change to 1 year
Caching -> Caching Level -> change to No Query String
Speed -> Autominify -> tick JS and CSS
Speed -> Rocket Loader -> change to automatic
Crypto -> SSL -> Change to FULL

Results with GTMetrix

The GTMetrix score, like all the page-speed analysers (Pingdom etc) doesn’t actually take server-response-speed into account in their score. That might sound odd, and it is.

The speed that matters the MOST for your website being able to scale is the TTFB – time to first byte. It’s important that uncached this stays below 500ms. You can install the Query Monitor plugin and you’ll see the uncached TTFB along your admin bar.

Running GTMetrix over a site I installed the above stack onto, I get the following results:

Note: you have to click the ‘Waterfall’ tab to view the TTFB speed:

That’s it.