Speedup WordPress with Nginx, HHVM, fastcgi-cache and W3 total Cache on Debian 8

After installing a wordpress for personal reasons, i had to suffer from an awefully slow loading time.
Even the default Theme with no content took literally ages to load.
So what can we do to enhance the speed of WordPress without using a CDN – we can also do this for images etc. pp later on.

This are my steps:

  • Installing
    • nginx
    • hhvm
    • wordpress
  • Adding W3 total cache with nginx
  • Adding nginx fast_cgi cache
  • Tweaking my theme and WordPress
  • Adding some more headers
  • Conclusion
  • Files

 

Installing

I’m asuming you have installed a fresh minimal Debian setup.
First we have to add dependencies to our sources.lst

nano /etc/apt/sources.list

Add these 3 lines:

deb http://nginx.org/packages/mainline/debian/ jessie nginx
deb-src http://nginx.org/packages/mainline/debian/ jessie nginx
deb http://dl.hhvm.com/debian jessie main

To Add the missing keys to our system, run the following commands:

apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 5A16E7281BE7A449
apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 ABF5BD827BD9BF62

Let’s now install some needed services:

apt-get update && apt-get install unzip curl wget build-essential mysql-client mysql-server nginx

As you can see, we don’t install php – this is due to the fact, that we want o use hhvm.
First let us create some directories, since debian want us to store our nginx-page-configs in /etc/nginx/conf.d/ but hhvm expect them in /etc/nginx/sites-enabled
We also move our configs from conf.d to sites-enabled – for convenience.

mkdir /etc/nginx/sites-enabled
mv /etc/nginx/conf.d/* /etc/nginx/sites-enabled/

Now e can install hhvm without getting errors

apt-get install hhvm
/usr/share/hhvm/install_fastcgi.sh
/etc/init.d/hhvm restart
update-rc.d hhvm defaults
/usr/bin/update-alternatives --install /usr/bin/php php /usr/bin/hhvm 60

How do i know the last 4 lines? Well after the installation of hhvm it tells me what to do πŸ™‚

Now we start with some tewaking of our nginx server, before adding a new page πŸ™‚
First let’s change our nginx.conf file that it looks s.th. like this:

user    www-data;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

worker_rlimit_nofile 65535;

events {
     worker_connections 2048;
        multi_accept on;
        use epoll;
}


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_types text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/richtext image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;

   fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:500m inactive=60m;
   fastcgi_cache_key "$scheme$request_method$host$request_uri";
   fastcgi_cache_use_stale error timeout invalid_header http_500;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

what do I have changed?
The first line goes from “user nginx” to “user www-data” – otherwise we are not able to have php remove the fastcgi-cached files – worker_processes should be set to the number of cores you have.
I also added worker_rlimit_nofile to be able to handle more than 1024 requests (see worker_connections).
The I activated gzip on the default types as well as adding a fastcgi_cache named WORDPRESS with a max-Size of 500MB.
Last but not Least, don’t forget to add the new folder to the settings, so that the configs will be loaded.

lease now install wordpress into your desired location. Please read another tutorial how to do this πŸ™‚ So here just some small settings:

cd /<your-wp-dir>/
wget http://wordpress.org/latest.zip
unzip latest.zip
rm latest.zip
cd wordpress
mv * ../
cd ..
rm -r wordpress
cp wp-config-sample.php wp-config.php

mysql -uroot -p
create database <db-name>;
GRANT ALL PRIVILEGES ON <db-name>.* TO <db-user>@'%' IDENTIFIED BY '<db-user-password>';
FLUSH PRIVILEGES;
exit

nano wp-config.php 
-> db settings
-> salts: put your unique phrase here
change table_prefix

if we are on apache – we are done now – but since we want to have a fast system, we have to configure our nginx πŸ™‚
go into your nginx folder -> /etc/nginx
create a new folder named includes, move the hhvm.conf into ths folder and create a new file in it:

mkdir /etc/nginx/includes
mv /etc/nginx/hhvm.conf /etc/nginx/includes/
nano /etc/nginx/includes/global_wp.conf 

In the global_wp.conf we add the global settings, as you can read on the wordpress-setup-guide: https://codex.wordpress.org/Nginx

# Global restrictions configuration file.
# Designed to be included in any server {} block.

location = /favicon.ico {
        log_not_found off;
        access_log off;
}

location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
}

# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~ /\. {
        deny all;
}

# Deny access to any files with a .php extension in the uploads directory
# Works in sub-directory installs and also in multisite network
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
}

Write and close the editor.

now let’s setup the virtual-host for your wordpress-page in /etc/nginx/sites-enbaled/mysite.conf:

server {
        listen 80;
        listen [::]:80;

        root /<your-wp-dir>/;
        index index.php index.html index.htm;

        access_log /dev/null;

        server_name my-domain.tld www.my-domain.tld;

        include includes/global_wp.conf;
        include includes/hhvm.conf;

        # Add trailing slash to */wp-admin requests.
        rewrite /wp-admin$ $scheme://$host$uri/ permanent;

        location / {
                try_files $uri $uri/ /index.php?$args ;
        }
}

after restarting nginx – and if you don’t have made some error, you can access and install your wordpess πŸ™‚

 

Adding w3 total cache with nginx

Download the Plugin and install it in your WordPress – take a look at the Problems it will say to you, especially if oyu haven’t setup a ftp-access.

e.g. create a nginx.conf file in your wordpress-folder and setting the right permissions.
for the setup and config of W3 Total Cache chmod your wp-content folder to 777 (don’t forget to change it back to 755, after you are finished configuring)

A new “Performance” menu-item will show up. Klick on it and go the General Settings.
Dont’ use “Toggle all caching types on or off (at once)” since it will activate everything – even the CDN-thing.

So what do we like to configure here?
Well … nearly everything.
I’m going to post the settings i took here.

w3_page_cache

If you have enough RAM – use APC, if not, use “Disk Enhanced” πŸ™‚
As for APC – hhvm has apc already installed in the default settings – nothing to do there.

w3_minfy

Set this to Manual Minfy – since Auto will do some strange things and won’t work.

w3_object

w3_db

w3_object

w3_browser

 

Now let’s look into the detailed Settings in the left menu:

I’m not exactly sure what is changed to the default settings, but as for minfy – take care what you remove. Especially removing line-breaks can break your design.

Page_Cache_W3_Total_Cache Minify_W3_Total_Cache

The other settings are “out-of-the-box” settings of W3C

After the configuration and de de-chmotting of our wp-content-folder, we have to add new infos to our virtual-host-config:

server {
        listen 80;
        listen [::]:80;

        root /<your-wp-dir>/;
        index index.php index.html index.htm;

        access_log /dev/null;

        server_name my-domain.tld www.my-domain.tld;

        include includes/global_wp.conf;
        include includes/wordpress-w3-total-cache.conf;
        include /<your-wp-dir>/nginx.conf;
        include includes/hhvm.conf;

        # Add trailing slash to */wp-admin requests.
        rewrite /wp-admin$ $scheme://$host$uri/ permanent;
        
        # disable access to nginx.conf of w3c
        location /nginx.conf {
                deny all;
        }

        location / {
                 try_files /wp-content/w3tc/pgcache/$cache_uri/_index.html $uri $uri/ /index.php?$args ;
        }
}

As you can see, we have changed our location /
this is due to the fact, that i’m using disk-caching for the page – since my site has nearly none existing visitors, there isn’t a problem with high IO-load πŸ™‚
we also deny access to the nginx.conf file within our wordpress-folder. There w3c will store it’s settings for nginx – that we have to load.
so, we also need to include this config within the virtual host πŸ™‚
the last include added is the default w3c-settings, which I also put into the includes folder.

Heres the default config – which you don’t need to change:

#W3 TOTAL CACHE CHECK
set $cache_uri $request_uri;

# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
        set $cache_uri 'null cache';
}
if ($query_string != "") {
        set $cache_uri 'null cache';
}

# Don't cache uris containing the following segments
if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comme$
        set $cache_uri 'null cache';
}

# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
        set $cache_uri 'null cache';
}
#ADD mobile rules from WP SUPER CACHE section above

#APPEND A CODE BLOCK FROM BELOW...

Voila w3c is installed and working
Don’t forget to de-chmod your wp-content-folder!

 

Adding nginx fast_cgi cache

So, let’s make the site-generation even more faster.
Wordpress uses a lots of “dynamic” php-code which will generate infos into the site – like the widget “last comment”
to speed this up, we will use fastcgi-cache provided by nginx.

When we edited the nginx.conf earlier in this post, we added the cache-definitions:

fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:500m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;

We here say, that our cache is 500MB in total size, named WORDPRESS and mark the files invalid after 60m within our cache. For sites without much updates, you can increase the inactive parameter to your liking – as well as the name and size of the cache.
Unfortionately you must not change the fastcgi_cache_key – don’t do this! We have to stick with it, since hhvm isn’t yet completly cpmpatible with the purge-options of php.
If we got an error, timeout, invalid header or we messed s.th. up (error 500) we permit to use an even older version of the cache – that may be invalid based on your time settings. But better that, than letting the users see an error – BUT how do we notice the errors then?
Well – we are setting up some exceptions for the caching – e.g. for logged in users πŸ™‚

But first – let’s activate the caching.
Therefore open the hhvm config and add the following lines:

fastcgi_cache_bypass $no_cache;
fastcgi_no_cache $no_cache;
fastcgi_cache WORDPRESS;
fastcgi_cache_valid 200 60m;

We here tell the hhvm what to do.
for example: if $no_cache is set – don’t cache
use the cache named WORDPRESS
tell him to cache files with return-code 200 (all OK) for 60minutes

Now we have somewhere to define the $no_cache variable.
we will do this within our virtual-host config

Add the folowing lines before the location /nginx.conf

        #fastcgi_cache start
        set $no_cache 0;

        # POST requests and urls with a query string should always go to PHP
        if ($request_method = POST) {
                set $no_cache 1;
        }
        if ($query_string != "") {
                set $no_cache 1;
        }

        # Don't cache uris containing the following segments
        if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
                set $no_cache 1;
        }

        # Don't use the cache for logged in users or recent commenters
        if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
                set $no_cache 1;
        }

all we are doing here is setting up the varialbe and setting some exceptions. Look at the comments for more infos.

But how do we remove cache-settings, if we updated something?
Well – luckyliy there is an nginx-helper-plugin available at wordpress.
Go here, download and install it.

Now for the config here is what i have done:

Nginx_Helper

You probably don’t need to flush the archive sites, but i’m so small that this doesn’t matter to me.
It’s important to set the “Purge Method” to “Delete local server cache files”. This is due to some incomaptibilities with hhvm.
Think about activating the Logging for the start so see, what is cached – and if there are some errors.

That’s it πŸ™‚

 

Tweaking my theme and WordPress

Well – not really. I want to minimize requests – especially to other services.
So first – let’s remove the requests to goolge.
There is a small plugin called “Disable Google Fonts” which will deactivate the default references within wordpress (it’s working fine with the Version 4.3.1).
My design also requests some fonts from google – so I manually removed this from the source-code. You really need to look if you want to do this – most of the themes will then look really shitty : D
So what can we do more?
I’ve also installed jQuery lazy load plugin to only load the images when you need them.

 

Adding some more headers

Well it’s loading really fast – but it would be even faster, if we tell the browser to cache also cache everything static we have.
Adding the following lines into the nginx.conf will tell the browser to cache them πŸ™‚

# Directives to send expires headers and turn off 404 error logging.
location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|woff|woff2)$ {
       access_log off; log_not_found off; expires max;
}

Conclusion

Just take a look at the results of webpagetest and pingdom:

result_pingdom result_webpagetest

So there’s nothing more to say. My site is fast enough : D

Happy Tuning! Dont’ forget to not doing this on a live system πŸ™‚

 

Files

nginx.conf

user    www-data;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

worker_rlimit_nofile 65535;

events {
#    worker_connections  1024;
     worker_connections 2048;
        multi_accept on;
        use epoll;
}


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;
    #tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;
    gzip_types text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/richtext image/svg+xml text/plain text/xsd text/xsl text/xml image/x-icon;

   fastcgi_cache_path /var/run/nginx-cache levels=1:2 keys_zone=WORDPRESS:500m inactive=60m;
   fastcgi_cache_key "$scheme$request_method$host$request_uri";
   fastcgi_cache_use_stale error timeout invalid_header http_500;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

virtual-host.conf

server {
        listen 80;
        listen [::]:80;

        root /<your-wp-dir>/;
        index index.php index.html index.htm;

        access_log /dev/null;

        server_name <your_domain.tld>;

        include includes/global_wp.conf;
        include includes/wordpress-w3-total-cache.conf;
        include /<your-wp-dir>/nginx.conf;
        include hhvm.conf;

        #fastcgi_cache start
        set $no_cache 0;

        # POST requests and urls with a query string should always go to PHP
        if ($request_method = POST) {
                set $no_cache 1;
        }
        if ($query_string != "") {
                set $no_cache 1;
        }

        # Don't cache uris containing the following segments
        if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)") {
                set $no_cache 1;
        }

        # Don't use the cache for logged in users or recent commenters
        if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
                set $no_cache 1;
        }

        # Add trailing slash to */wp-admin requests.
        rewrite /wp-admin$ $scheme://$host$uri/ permanent;

        # Directives to send expires headers and turn off 404 error logging.
        location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|woff|woff2)$ {
               access_log off; log_not_found off; expires max;
        }

        location /nginx.conf {
                deny all;
        }

        location / {
                try_files /wp-content/w3tc/pgcache/$cache_uri/_index.html $uri $uri/ /index.php?$args ;
        }
}

hhvm.conf


location ~ \.(hh|php)$ {
    fastcgi_keep_conn on;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;

        fastcgi_cache_bypass $no_cache;
         fastcgi_no_cache $no_cache;

         fastcgi_cache WORDPRESS;
         fastcgi_cache_valid 200 60m;
    include        fastcgi_params;
}

Leave a Reply

Your email address will not be published. Required fields are marked *