Just going to go straight into the commands. This assumes you’ve configured your DNS and started a Digital Ocean droplet and have created a root-empowered user. If you’re looking to start using Digital Ocean, get $100 credit with Digital Ocean (Referral link, win-win). This example was performed on a 1 CPU, 2 GB RAM droplet.

Create a Swap File

<span class="token function">sudo</span> fallocate -l 4G /swapfile
<span class="token function">sudo</span> <span class="token function">chmod</span> 600 /swapfile
<span class="token function">sudo</span> mkswap /swapfile
<span class="token function">sudo</span> swapon /swapfile
<span class="token keyword">echo</span> <span class="token string">'/swapfile none swap sw 0 0'</span> <span class="token operator">|</span> <span class="token function">sudo</span> <span class="token function">tee</span> -a /etc/fstab
<span class="token function">sudo</span> swapon --show

Percona Server for MySQL 8.0.13-3

Fetch, install, and enable the repository then install the server package.


<span class="token function">wget</span> https://repo.percona.com/apt/percona-release_latest.<span class="token variable"><span class="token variable">$(</span>lsb_release -sc<span class="token variable">)</span></span>_all.deb
<span class="token function">sudo</span> dpkg -i percona-release_latest.<span class="token variable"><span class="token variable">$(</span>lsb_release -sc<span class="token variable">)</span></span>_all.deb
<span class="token function">sudo</span> percona-release setup ps80
<span class="token function">sudo</span> <span class="token function">apt-get</span> update -y <span class="token operator">&&</span> <span class="token function">sudo</span> <span class="token function">apt-get</span> upgrade -y
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> percona-server-server
<span class="token function">sudo</span> update-rc.d mysql defaults

With WordPress 5.0, the MySQL connector does not support caching_sha2 authentication. You’ll have to use “mysql_native_password.”


<span class="token keyword">CREATE</span> <span class="token keyword">user</span> <span class="token string">'example'</span>@'localhost<span class="token string">' identified with mysql_native_password by '</span>example <span class="token number">123</span><span class="token string">';
grant all privileges on example.* to '</span>example<span class="token string">'@'</span>localhost'<span class="token punctuation">;</span>

How you optimize your MySQL installation really depends on your usage. For WordPress, I would at least allocate 256MB for the buffer pool. Let the site run for a few weeks and figure out how to optimize it then.

<span class="token constant">innodb_buffer_pool_size</span> <span class="token attr-value"><span class="token punctuation">=</span> 256M</span>

Install PHP 7.3.0-2


<span class="token function">sudo</span> add-apt-repository ppa:ondrej/php
<span class="token function">sudo</span> <span class="token function">apt-get</span> update
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> php7.3
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> php7.3-fpm php7.3-cli php7.3-mysql php7.3-gd php7.3-imagick php-redis php7.3-recode php7.3-tidy php7.3-xmlrpc php7.3-mbstring php-xml php-xmlrpc 

<span class="token function">sudo</span> vim /etc/php/7.3/fpm/pool.d/www.conf 

This setting really depends on how much ram you have available. Find out how much each PHP process takes up and divide the amount of free memory by that number.

<span class="token function">ps</span> --no-headers -o <span class="token string">"rss,cmd"</span> -C php-fpm7.3 <span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{ sum+=<span class="token variable">$1</span> } END { printf ("%d%s\n", sum/NR/1024,"Mb") }'</span>

In my case, I have about 1,256MB free, each process runs about 110MB, so that gives 11 to be safe.

<span class="token constant">pm</span> <span class="token attr-value"><span class="token punctuation">=</span> ondemand</span>
<span class="token constant">pm.process_idle_timeout</span> <span class="token attr-value"><span class="token punctuation">=</span> 10s</span>
<span class="token constant">pm.max_children</span> <span class="token attr-value"><span class="token punctuation">=</span> 11</span>
<span class="token constant">pm.start_servers</span> <span class="token attr-value"><span class="token punctuation">=</span> 5</span>
<span class="token constant">pm.min_spare_servers</span> <span class="token attr-value"><span class="token punctuation">=</span> 5</span>
<span class="token constant">pm.max_spare_servers</span> <span class="token attr-value"><span class="token punctuation">=</span> 10</span>

The default values for php.ini are a bit low for themes and performance.

<span class="token function">sudo</span> vim /etc/php/7.3/fpm/php.ini 
<span class="token constant">upload_max_filesize</span> <span class="token attr-value"><span class="token punctuation">=</span> 100M</span>
<span class="token constant">memory_limit</span> <span class="token attr-value"><span class="token punctuation">=</span> 256M</span>

Install Nginx


<span class="token function">sudo</span> <span class="token function">apt-get</span> purge apache2
<span class="token function">sudo</span> apt autoremove
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> fail2ban
<span class="token function">sudo</span> apt <span class="token function">install</span> nginx
<span class="token function">sudo</span> ufw app list
<span class="token function">sudo</span> ufw allow <span class="token string">'OpenSSH'</span>
<span class="token function">sudo</span> ufw allow <span class="token string">'Ngninx Full'</span>
<span class="token function">sudo</span> ufw status
<span class="token function">sudo</span> systemctl nginx status
curl example.com

The relationship between Nginx and PHP-FPM workers and processes…

There’s a lot of confusion between Nginx workers and php-fpm workers. First of all, all workers are processes – and all processes consume memory. The amount of memory that a process could take (such as php-fpm7.3), depends on your CMS/application (WordPress plugins, themes, etc). I’ve had some WordPress sites take over 100MB of memory per php-fpm worker.

A single Nginx worker could asynchronously handle over 1,000 requests per second. When php-fpm receives a request from Nginx, it’ll have to allocate memory for a new php-fpm process or use an existing one. So even if php-fpm complains with “[pool www] server reached pm.max_children setting (11), consider raising it,” you’re limited by the amount of RAM you have. If you increase beyond that – your server will go down with heavy traffic.

So the number of Nginx workers are static (from your configuration) but the number of php-fpm workers you have will vary depending on the number of requests and generally the amount of RAM you have. So even though Nginx could handle 1,000 requests, it doesn’t mean your server can.

Setup Server Block (Virtual Host)


<span class="token function">sudo</span> <span class="token function">mkdir</span> -p /var/www/example.com/
<span class="token function">sudo</span> <span class="token function">chown</span> -R www-data:www-data /var/www/example.com/
<span class="token function">sudo</span> <span class="token function">chmod</span> -R 755 /var/www/example.com/
<span class="token function">sudo</span> vim /etc/nginx/sites-available/example.com
<span class="token function">sudo</span> <span class="token function">wget</span> https://wordpress.org/latest.zip
<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> unzip -y
<span class="token function">sudo</span> unzip latest.zip
<span class="token function">sudo</span> <span class="token function">rsync</span> -av wordpress/* /var/www/example.com/
<span class="token function">sudo</span> <span class="token function">chown</span> -R www-data:www-data /var/www/example.com/
<span class="token function">sudo</span> <span class="token function">chmod</span> -R 755 /var/www/example.com

<span class="token keyword">server</span> <span class="token punctuation">{</span>
        <span class="token keyword">listen</span> <span class="token number">80</span><span class="token punctuation">;</span>
        <span class="token keyword">listen</span> <span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">:</span><span class="token number">80</span><span class="token punctuation">;</span>

        <span class="token keyword">client_max_body_size</span> 25m<span class="token punctuation">;</span>

        <span class="token keyword">root</span> <span class="token operator">/</span>var<span class="token operator">/</span>www<span class="token operator">/</span>example<span class="token punctuation">.</span>com<span class="token punctuation">;</span>
        <span class="token keyword">index</span> <span class="token keyword">index</span><span class="token punctuation">.</span>php <span class="token keyword">index</span><span class="token punctuation">.</span>html <span class="token keyword">index</span><span class="token punctuation">.</span>htm <span class="token keyword">index</span><span class="token punctuation">.</span>nginx<span class="token operator">-</span>debian<span class="token punctuation">.</span>html<span class="token punctuation">;</span>

        <span class="token keyword">server_name</span> example<span class="token punctuation">.</span>com www<span class="token punctuation">.</span>example<span class="token punctuation">.</span>com<span class="token punctuation">;</span>

        <span class="token keyword">location</span> <span class="token operator">/</span> <span class="token punctuation">{</span>
                <span class="token keyword">try_files</span> <span class="token variable">$uri</span> <span class="token variable">$uri</span><span class="token operator">/</span> <span class="token operator">=</span><span class="token number">404</span> <span class="token operator">/</span><span class="token keyword">index</span><span class="token punctuation">.</span>php<span class="token operator">?</span><span class="token variable">$args</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    
        <span class="token keyword">location</span> <span class="token operator">~</span> \<span class="token punctuation">.</span>php$ <span class="token punctuation">{</span>
          <span class="token keyword">include</span> snippets<span class="token operator">/</span>fastcgi<span class="token operator">-</span>php<span class="token punctuation">.</span>conf<span class="token punctuation">;</span>
          <span class="token keyword">fastcgi_pass</span> unix<span class="token punctuation">:</span><span class="token operator">/</span>var<span class="token operator">/</span>run<span class="token operator">/</span>php<span class="token operator">/</span>php7<span class="token number">.3</span><span class="token operator">-</span>fpm<span class="token punctuation">.</span>sock<span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token function">sudo</span> <span class="token function">ln</span> -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
<span class="token function">sudo</span> systemctl reload nginx
 
#uncomment server_names_hash_bucket_size in /etc/nginx/nginx.conf
server_names_hash_bucket_size 64;
 
<span class="token function">sudo</span> nginx -t
<span class="token function">sudo</span> systemctl restart nginx

At this point, you should see the WordPress installation page.

Enable HTTP Strict Transport Security (HSTS) and Gzip

It’s recommended to use HSTS even if you redirect. Browsers tend to cache 301 responses.  Edit your /etc/nginx/nginx.conf file and add the following add_header section:

<span class="token keyword">worker_processes</span> <span class="token punctuation">[</span><span class="token comment"># of processors on your server];</span>
<span class="token keyword">server_tokens</span> off<span class="token punctuation">;</span> <span class="token comment">#great for security reasons</span>

<span class="token keyword">location</span> <span class="token operator">~</span> <span class="token operator">/</span>\<span class="token punctuation">.</span> <span class="token punctuation">{</span>
  <span class="token keyword">access_log</span> off<span class="token punctuation">;</span>
  <span class="token keyword">log_not_found</span> off<span class="token punctuation">;</span>
  <span class="token keyword">deny</span> all<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">include</span> <span class="token operator">/</span>etc<span class="token operator">/</span>nginx<span class="token operator">/</span>conf<span class="token punctuation">.</span>d<span class="token operator">/</span><span class="token operator">*</span><span class="token punctuation">.</span>conf<span class="token punctuation">;</span>
<span class="token keyword">include</span> <span class="token operator">/</span>etc<span class="token operator">/</span>nginx<span class="token operator">/</span>sites<span class="token operator">-</span>enabled<span class="token operator">/</span><span class="token operator">*</span><span class="token punctuation">;</span>
<span class="token keyword">add_header</span> Strict<span class="token operator">-</span>Transport<span class="token operator">-</span>Security <span class="token string">"max-age=7884008; includeSubDomains"</span> always<span class="token punctuation">;</span>

<span class="token comment">#Since we're here, let's enable gzip</span>
<span class="token keyword">gzip</span> on<span class="token punctuation">;</span>
<span class="token keyword">gzip_vary</span> on<span class="token punctuation">;</span>
<span class="token keyword">gzip_proxied</span> any<span class="token punctuation">;</span>
<span class="token keyword">gzip_comp_level</span> <span class="token number">6</span><span class="token punctuation">;</span>
<span class="token keyword">gzip_buffers</span> <span class="token number">16</span> 8k<span class="token punctuation">;</span>
<span class="token keyword">gzip_http_version</span> <span class="token number">1.1</span><span class="token punctuation">;</span>
<span class="token keyword">gzip_types</span> text<span class="token operator">/</span>plain text<span class="token operator">/</span>css application<span class="token operator">/</span>json application<span class="token operator">/</span>javascript text<span class="token operator">/</span>xml application<span class="token operator">/</span>xml application<span class="token operator">/</span>xml<span class="token operator">+</span>rss text<span class="token operator">/</span>javascript<span class="token punctuation">;</span>

Let’s Encrypt – Add SSL


<span class="token comment"># Enter email and make redirect to HTTPS</span>
<span class="token function">sudo</span> apt <span class="token function">install</span> python-certbot-nginx
<span class="token function">sudo</span> certbot --nginx -d www.example.com -d example.com

Adding HTTP/2 Support

Just add “http2” in these sections of your /etc/nginx/sites-available/example.com

You should see “HTTP/2 200” in the response after running the curl command.


<span class="token keyword">listen</span> <span class="token punctuation">[</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token punctuation">]</span><span class="token punctuation">:</span><span class="token number">443</span> <span class="token keyword">ssl</span> http2<span class="token punctuation">;</span> <span class="token comment"># managed by Certbot</span>
<span class="token keyword">listen</span> <span class="token number">443</span> <span class="token keyword">ssl</span> http2<span class="token punctuation">;</span> <span class="token comment"># managed by Certbot</span>
 
<span class="token comment"># command line</span>
<span class="token function">sudo</span> systemctl reload nginx
curl -I -L https://example.com
 # You should see HTTP/2 200 server: nginx/1.14.0 (Ubuntu) date: Wed, 26 Dec 2018 23:28:17 GMT content-type: text/html; charset=utf-8 expires: Wed, 11 Jan 1984 05:00:00 GMT cache-control: no-cache, must-revalidate, max-age=0

iFrame

WordPress requires iFrames for plugin details and installation results. Edit /etc/nginx/snippets/ssl-params.conf

# add_header X-Frame-Options DENY always;
add_header X-Frame-Options SAMEORIGIN;

Install Redis

Keep in mind just because you restart Redis doesn’t mean it won’t start up with data. Depending on how you have it configured – Redis might’ve snapshotted some data. Make sure you run “redis-cli flushall” if you want it cleared. This could cause funky things to happen – especially if you have CSS links to a CDN.


<span class="token function">sudo</span> <span class="token function">apt-get</span> <span class="token function">install</span> redis -y
<span class="token function">sudo</span> vim /etc/redis/redis.conf
 
#add this to the bottom. This specific server has 2GB of memory.
maxmemory 200mb
maxmemory-policy allkeys-lru

# Also, comment out the following lines in redis.conf. 
# This will ensure a 100% memory cache.
#save 900 1
#save 300 10
#save 60 10000
 
<span class="token function">sudo</span> systemctl restart redis-server

Enable Cache-control

Add this above your server block in the /etc/nginx/sites-available/example.com configuration file. Be sure to enter the one-liner in the server block as well.


<span class="token keyword">map</span> <span class="token variable">$sent_http_content_type</span> <span class="token variable">$expires</span> <span class="token punctuation">{</span>
    default                    off<span class="token punctuation">;</span>
    text<span class="token operator">/</span>html                  epoch<span class="token punctuation">;</span>
    text<span class="token operator">/</span>css                   max<span class="token punctuation">;</span>
    application<span class="token operator">/</span>javascript     max<span class="token punctuation">;</span>
    <span class="token operator">~</span>image<span class="token operator">/</span>                    max<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">server</span> <span class="token punctuation">{</span>
  <span class="token keyword">root</span> <span class="token operator">/</span>var<span class="token operator">/</span>www<span class="token operator">/</span>example<span class="token punctuation">.</span>com<span class="token operator">/</span><span class="token punctuation">;</span>
  <span class="token keyword">index</span> <span class="token keyword">index</span><span class="token punctuation">.</span>php <span class="token keyword">index</span><span class="token punctuation">.</span>html <span class="token keyword">index</span><span class="token punctuation">.</span>htm <span class="token keyword">index</span><span class="token punctuation">.</span>nginx<span class="token operator">-</span>debian<span class="token punctuation">.</span>html<span class="token punctuation">;</span>
  <span class="token keyword">expires</span> <span class="token variable">$expires</span><span class="token punctuation">;</span>

Enable Nginx FastCGI Cache

Huge speed gains with Nginx FastCGI cache.


<span class="token function">sudo</span> <span class="token function">mkdir</span> -p /var/cache/nginx/fastcgi-cache/cache

Edit /etc/nginx/nginx.conf in the “http” block. Size (256m) has to be less than your RAM + swap space. You may also want to use your tmpfs mounts (such as /run) to run the cache in memory. Just be careful what you set your maximum value at.


<span class="token keyword">fastcgi_cache_path</span> <span class="token operator">/</span>var<span class="token operator">/</span>cache<span class="token operator">/</span>nginx<span class="token operator">/</span>fastcgi<span class="token operator">-</span>cache<span class="token operator">/</span>cache levels<span class="token operator">=</span><span class="token number">1</span><span class="token punctuation">:</span><span class="token number">2</span> keys_zone<span class="token operator">=</span>fastcgizone<span class="token punctuation">:</span>256m max_size<span class="token operator">=</span>2g inactive<span class="token operator">=</span>60m use_temp_path<span class="token operator">=</span>off<span class="token punctuation">;</span>
<span class="token keyword">fastcgi_cache_key</span> <span class="token variable">$scheme</span><span class="token variable">$request_method</span><span class="token variable">$host</span><span class="token variable">$request_uri</span><span class="token punctuation">;</span>
<span class="token keyword">fastcgi_cache_lock</span> on<span class="token punctuation">;</span>
fastcgi_cache_revalidate on<span class="token punctuation">;</span>
fastcgi_cache_background_update on<span class="token punctuation">;</span>
<span class="token keyword">fastcgi_cache_use_stale</span> error <span class="token keyword">timeout</span> invalid_header updating http_500<span class="token punctuation">;</span>
<span class="token keyword">fastcgi_cache_valid</span> <span class="token number">200</span> 60m<span class="token punctuation">;</span>
<span class="token keyword">fastcgi_pass_header</span> <span class="token keyword">Set</span><span class="token operator">-</span>Cookie<span class="token punctuation">;</span>
<span class="token keyword">fastcgi_pass_header</span> Cookie<span class="token punctuation">;</span>
<span class="token keyword">fastcgi_ignore_headers</span> Cache<span class="token operator">-</span>Control <span class="token keyword">Expires</span> <span class="token keyword">Set</span><span class="token operator">-</span>Cookie<span class="token punctuation">;</span>

Keep in mind you should have a separate cache path/zone for each virtual host. Now go back and edit the /etc/nginx/sites-available/example.com configuration file:


<span class="token keyword">location</span> <span class="token operator">~</span> \<span class="token punctuation">.</span>php$ <span class="token punctuation">{</span>
  <span class="token keyword">include</span> snippets<span class="token operator">/</span>fastcgi<span class="token operator">-</span>php<span class="token punctuation">.</span>conf<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_pass</span> unix<span class="token punctuation">:</span><span class="token operator">/</span>var<span class="token operator">/</span>run<span class="token operator">/</span>php<span class="token operator">/</span>php7<span class="token number">.3</span><span class="token operator">-</span>fpm<span class="token punctuation">.</span>sock<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_param</span> SCRIPT_FILENAME <span class="token variable">$document_root</span><span class="token variable">$fastcgi_script_name</span><span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_cache_bypass</span> <span class="token variable">$skip_cache</span><span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_no_cache</span> <span class="token variable">$skip_cache</span><span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_cache</span> fastcgizone<span class="token punctuation">;</span>
  <span class="token keyword">include</span> fastcgi_params<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_buffer_size</span> 128k<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_connect_timeout</span> 60s<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_send_timeout</span> 60s<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_read_timeout</span> 60s<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_buffers</span> <span class="token number">256</span> 16k<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_busy_buffers_size</span> 256k<span class="token punctuation">;</span>
  <span class="token keyword">fastcgi_temp_file_write_size</span> 256k<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token keyword">set</span> <span class="token variable">$skip_cache</span> <span class="token number">0</span><span class="token punctuation">;</span>
  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$http_cookie</span> <span class="token operator">~</span><span class="token operator">*</span> <span class="token string">"comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">set</span> <span class="token variable">$skip_cache</span> <span class="token number">1</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$request_method</span> <span class="token operator">=</span> POST<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">set</span> <span class="token variable">$skip_cache</span> <span class="token number">1</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>   

  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$query_string</span> <span class="token operator">!=</span> <span class="token string">""</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">set</span> <span class="token variable">$skip_cache</span> <span class="token number">1</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

  <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$http_cookie</span> <span class="token operator">~</span><span class="token operator">*</span> <span class="token string">"comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">set</span> <span class="token variable">$skip_cache</span> <span class="token number">1</span><span class="token punctuation">;</span>
  <span class="token punctuation">}</span>

Install Autoptimize and Async Javascript Plugins

Autoptimize and Async Javascript help cuts another second off the load time and further improves the scores below.

The only thing I would be careful of is Async Javascript, it might break certain things. You may have to add “async” or “defer” to some of your <script> tags manually. Excluding your theme will probably help as well. The performance is still worth it.

The “Aggregate all linked JS-files to have them loaded non-render blocking? If this option is off, the individual JS-files will remain in place but will be minified.” setting in particular improves performance – but may cause a few errors. It shaved about 1.5 seconds off (HUGE!).

Results (with default theme):

I’ll do more testing with CDNs but this is a great way to maximize the utilization of your VPS.