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.

Update: Since writing this article, I’ve added a stack configuration guide too.

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.3 – 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

First, a bit of background

Before going into the stack build, I’d like to introduce myself. I’m Dave Hilditch, founder of WP Intense. I’ve been developing WordPress plugins focused on improving WordPress performance for the past 4 years after leaving Skyscanner. You can see me talking about WordPress performance issues at my talk at WordCamp in Brighton, UK.

Here’s what some of our customers have to say about our plugins:

This plugin is brilliant!!! Would give it 10 stars if I could. Brought the site loading times down to less than 2 seconds – was sitting at about 10 seconds before.

Corey D, talking about our Scalability Pro plugin

Bought external images and scalability pro. 5 stars for both of them. Thank you for these superb plugins which really boosted my website speed and product imports.

Markus P, talking about our External Images and Scalability Pro plugins

This is the third plugin i bought! Faster isn’t close to how fast woo widgets load! Excellent plugin!!!

Savvas Z, talking about our Faster Woo Widgets plugin

If you’re thinking about rebuilding your WordPress hosting stack for performance, you may find with our plugin pack that you don’t need to! Now onto the guide!

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 $50 credit towards your next server(s). That means you can build a great server with this stack and host it free for 2 or 3 months. To take advantage, click this Digital Ocean Affiliate Link.

If the guide below is too hardcore for you, we’ll soon be reviewing managed services. Our shortlist includes WP Engine, Kinsta, GridPane and Cloudways.

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


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.


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. NOTE: There is one exception – do not choose the default for the authentication mechanism when installing MySQL – use the LEGACY authentication mechanism to remain WordPress compatible. 

wget -c
dpkg -i mysql-apt-config_0.8.13-1_all.deb
apt install software-properties-common
add-apt-repository ppa:ondrej/php
apt-get update
apt-get upgrade
apt-get install mysql-server -y # accept all defaults
apt-get -y install php7.3
apt-get purge apache2 -y
apt-get install nginx -y
apt-get install -y tmux curl wget php7.3-fpm php7.3-cli php7.3-curl php7.3-gd php7.3-intl
apt-get install -y php7.3-mysql php7.3-mbstring php7.3-zip php7.3-xml unzip php7.3-soap php7.3-redis
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. 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 at 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
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
mkdir /var/www/cache/rocketstack
chown www-data:www-data /var/www/cache/ -R

Before restarting nginx, you should alter the rocketstack.conf file – specifically, you want to enter your own domain name to prevent botnets attacking your site via the IP address.

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

Then change the server_name _; line to read (there are 2 lines like this):


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 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 WITH mysql_native_password BY 'CHOOSEASTRONGPASSWORD';
GRANT ALL PRIVILEGES ON rocketstack.* TO'rs'@'localhost';

You can install WordPress using the following set of commands:

wget -P /var/www/
unzip /var/www/ -d /var/www/
mv /var/www/wordpress /var/www/rocketstack
chown www-data:www-data /var/www/rocketstack -R
rm /var/www/

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.

Troubleshooting WordPress database connection errors

If you are experiencing a database connection error, it is likely related to the new MySQL 8 authentication methods.

You can check your default authentication method by reading this file:

cat /etc/mysql/mysql.conf.d/default-auth-override.cnf

It should contain the following:

default-authentication-plugin = mysql_native_password

If it doesn’t, change it to mysql_native_password, restart mysql and then rerun your WordPress installation again by revisiting your home page. Edit the file using:

vi /etc/mysql/mysql.conf.d/default-auth-override.cnf

Restart mysql first using:

service mysql restart

Adding SSL using Letsencrypt

apt-get update
apt-get install software-properties-common
add-apt-repository universe
add-apt-repository ppa:certbot/certbot
apt-get update
apt-get install python-certbot-nginx
certbot --nginx

The last command above will scan your nginx configuration files and figure out any URLs you have set for this server based on the server_name variable. If you left them as _ you should change them to your domain name(s). If you have multiple domain names, you can separate them by spaces in the server_name variable.

The command will run and update your nginx config file to have the correct location of the SSL certificates.

Once complete, you need to set up SSL renewals, so run:

crontab -e

And add the following line:

0 0 1 * * certbot renew

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 references with 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.

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 // 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 domain name and choose ‘Always HTTPS’. Save and deploy.

You should already have your WordPress site URL set to so 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_log_file_size = 100M
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 ~
git clone

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.3/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 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.

You should also uncomment the line (uncomment by removing the leading semi-colon) which starts:

;max_input_vars = 1000

And then change the value to 5000 to accommodate some of the more badly written plugins (looking at you UberMenu!).

Finally, still inside your php.ini file you should enable opcache.


There are potentially some other optimisations that will help your specific scenario, so take the time to read through the comments in the php.ini file and decide for yourself it there’s anything else you need to tweak.

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.3/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.3-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.

Securing MySQL

MySQL by default will not be accessible from anywhere other than your localhost. So it’s already very secure. If you want to secure it further, you can run this command below but BE CAREFUL to choose the mysql_native_authentication rather than the new recommended cached authentication. If you make a mistake with this, check the database troubleshooting section above for how to fix.

mysql_secure_installation # choose y for everything EXCEPT recommended authentication plugin and enter a secure root password

Optional nginx snippets

There are other optional nginx snippets inside our github repo which you can include in your server blocks in /etc/nginx/sites-available/rocketstack.conf. You should take the time to take a look at these snippets and see which additional ones you might wish to include.

For example, you may wish to have nginx handle your gzip compression, so inside both your server blocks you would add the following line:

include snippets/gzip.conf

However – if you’re using Cloudflare or another CDN, you could instead have your CDN handle GZIP compression which will keep that CPU load away from your server.

You may also wish to use the Yoast SEO sitemaps config file. To do so, you’d include this line in both your server blocks:

include snippets/yoast-sitemaps.xml

You can view the various snippets we have on our rocket stack github repo, or if you’ve cloned the repo you can use your preferred file editor.

Testing everything individually

In addition to the specific debugging and troubleshooting sections above, I’ve been asked some times how to debug a failed install. Here’s how:

  1. Test you can connect to mysql
  2. Test nginx and DNS using a static HTML file
  3. Test PHP using a PHP file
  4. Test fastcgi_cache using any of your static files (css, js etc) – open developer console, and look in the network tab, look in the headers, look for NGINX: HIT rather than NGINX: BYPASS
  5. Test Redis using Redis plugin or the redis-cli command


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.

Update: Since writing this article, I’ve added a stack configuration guide too.

Talk to me
Latest posts by Dave Hilditch (see all)