Speed Up Your WordPress Site with Pound, Varnish, Nginx and mod_pagespeed

This post was contributed by Tomaž Zaman. He is the founder of a Danish startup called Codeable, a WordPress-only outsourcing service on a mission to help WordPress companies and enthusiasts from all over the world effortlessly scale their businesses. He spends his free time with his wife and four kids.

A vast majority of online articles about speeding up your WordPress site mention and use a plugin called W3 Total Cache (or W3TC for short). Rightfully so, because it’s a great, all-in-one solution to get your WP site fast with relatively small amount of work.

However, I’m not really a fan of all-in-one solutions, mainly because they bring a lot of complexity to the table, and as far as WordPress plugins go, possibly introduce compatibility issues and upgrade headaches.

That is why I decided to look for alternatives that would still make our (secure) WordPress site really fast and a friend recommended to look at mod_pagespeed, which turned out to be a great solution for making our WordPress really fast, with minimal effort.

Prerequisites

This article assumes you have at least basic knowledge of linux (all our examples are Ubuntu-based), know how to use the shell, and most importantly, you host your site on your own VPS. Shared hosting isn’t going to cut it, since we need to set up a customized version of nginx, which supports mod_pagespeed.

There are plenty of awesome and relatively cheap VPS providers out there, I’d personally recommend Digital Ocean.

Note that all commands that start with the dollar sign ($) indicate it’s a unix command, and you don’t actually enter that dollar sign into the command.

Main ingredients

As the title suggests, we’ll need a couple of programs installed on our server; The main one (for caching purposes) is called Varnish, which basically stores all your HTML output onto a temporary folder on disk and serves that instead of delegating requests to WordPress. It has one flaw, however – it does not support SSL termination, which is why we need Pound for.

The last two components we need are Nginx (it’s a web server like Apache) and a PHP process manager called PHP-FPM, since Nginx does not support modules.

Of course you also need to generate a certificate key and buy a certificate for this setup to work. Entry-level certificates are quite cheap, some starting with less that $10/year, which makes it a no-brainer.

Getting started

Provided you have a clean, fresh VPS ready, SSH on to it. Protip: If you use Linux or Mac OS locally, you can add the following a shortcut to ~/.ssh/config(create this file if it does not exist):

[code]
Host MyHost
HostName [host-ip]
Port [host-port]
User [host user, usually root]
IdentityFile [path/to/your/ID_RSA, usually ~/.SSH/ID_RSA]
[/code]

This allows you to run the following command to log into your server shell: $ ssh MyHost.

Once logged in, it’s time to install all the necessary components that we’ll need via a package manager, in our case apt-get.

Run the following command: $ sudo apt-get install php5-fpm php5-cli php5-mysql varnish pound mariadb-server-5.5 unzip You’ll be prompted to enter the root password for MariaDB, which is a drop-in replacement for MySQL, twice. We did not install Nginx at this point because the version that comes as the default, does not support mod_pagespeed, which we need, so we’ll install it manually. To do that, visit the official documentation and follow the guide, with one exception, replace this line:

[code lang=text]
./configure –add-module=$HOME/ngx_pagespeed-release-${NPS_VERSION}-beta
[/code]

with this one:

[code lang=text]
./configure –add-module=$HOME/ngx_pagespeed-release-${NPS_VERSION}-beta –with-http_gzip_static_module –with-http_realip_module
[/code]

This will make sure you also include modules that will help you GZIP static assets and allow you to track correct IPs in your WordPress, respectively. Once done, you should have Nginx installed in /usr/local/nginx/.

If you try to visit the URL (or IP) of your server at this point, nothing will happen, because we haven’t configured all the components properly yet. Once we do, the communication schema between components on our server will look something like this:

Pound

Let’s start with the outermost component, which should listen on two standard ports, 80 and 443 (HTTP and HTTPS, respectively). Edit the file /etc/pound/pound.cfg and put the following contents in:

[code lang=text]
User "www-data"
Group "www-data"
LogLevel 1
Alive 30
Control "/var/run/pound/poundctl.socket"

ListenHTTP
Address 0.0.0.0
Port 80
# This part makes sure you redirect all HTTP traffic to HTTPS
Service
HeadRequire "Host: yourdomain.com"
Redirect "https://yourdomain.com"
End
End
ListenHTTPS
HeadRemove "X-Forwarded-Proto"
AddHeader "X-Forwarded-Proto: https"
Address 0.0.0.0
Port 443
Cert "/etc/ssl/yourdomain.com/yourdomain.com.pem"
# This service removes the WWW-part
Service
HeadRequire "Host: www.mydomain.com"
Redirect "https://yourdomain.com"
End
# The main service which passes requests to Varnish
Service
HeadRequire "Host: yourdomain.com"
BackEnd
Address 127.0.0.1
# 6081 is the default Varnish port
Port 6081
End
End
End
[/code]

Before restarting pound with this new configuration, you’ll also need to have a PEM certificate file in the defined folder, which you can create by following these instructions. You’ll also need to edit /etc/default/pound and set startup=1.

That’s it! Pound is now configured and after running $ service pound restart (on Ubuntu, other distributions may have different commands for service manipulation) you should be able to visit the configured domain. Of course, you’ll get an error (503 Service Unavailable), but that’s okay, we’re not done yet!

Varnish

Next, we need to configure Varnish. Edit the file `/etc/varnish/default.vcl and put the following in (delete everything else):

[code lang=text]
backend default {
.host = "127.0.0.1";
.port = "8080";
}

acl purge {
"127.0.0.1";
"localhost";
}

sub vcl_recv {
if (req.request == "PURGE") {
if (!client.ip ~ purge) {
error 405 "Not allowed.";
}
ban("req.url ~ "+req.url+" && req.http.host == "+req.http.host);
error 200 "OK";
}

# only using one backend
set req.backend = default;

# set standard proxied ip header for getting original remote address
set req.http.X-Forwarded-For = client.ip;

# logged in users must always pass
if( req.url ~ "^/wp-(login|admin)" || req.http.Cookie ~ "wordpress_logged_in_" ){
return (pass);
}

# don't cache search results
if( req.url ~ "\?s=" ){
return (pass);
}

# always pass through posted requests and those with basic auth
if ( req.request == "POST" || req.http.Authorization ) {
return (pass);
}

# else ok to fetch a cached page
unset req.http.Cookie;
return (lookup);
}

sub vcl_fetch {

# remove some headers we never want to see
unset beresp.http.Server;
unset beresp.http.X-Powered-By;

# only allow cookies to be set if we're in admin area – i.e. commenters stay logged out
if( beresp.http.Set-Cookie && req.url !~ "^/wp-(login|admin)" ){
unset beresp.http.Set-Cookie;
}

# don't cache response to posted requests or those with basic auth
if ( req.request == "POST" || req.http.Authorization ) {
return (hit_for_pass);
}

# only cache status ok
if ( beresp.status != 200 ) {
return (hit_for_pass);
}

# don't cache search results
if( req.url ~ "\?s=" ){
return (hit_for_pass);
}

# else ok to cache the response
set beresp.ttl = 24h;
return (deliver);
}

sub vcl_deliver {
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
}
else {
set resp.http.X-Cache = "MISS";
}
unset resp.http.Via;
unset resp.http.X-Varnish;
}

sub vcl_hit {
if (req.request == "PURGE") {
purge;
error 200 "OK";
}
}

sub vcl_miss {
if (req.request == "PURGE") {
purge;
error 404 "Not cached";
}
}

sub vcl_hash {
hash_data( req.url );
if ( req.http.host ) {
hash_data( regsub( req.http.host, "^([^\.]+\.)+([a-z]+)$", "\1\2" ) );
} else {
hash_data( server.ip );
}
# ensure separate cache for mobile clients (WPTouch workaround)
if( req.http.User-Agent ~ "(iPod|iPhone|incognito|webmate|dream|CUPCAKE|WebOS|blackberry9\d\d\d)" ){
hash_data("touch");
}
return (hash);
}
[/code]

I won’t go into much details here, because official Varnish documentation does a much better job at that. Three things worth mentioning here are the lines that PURGE the cache (delete it), which comes in handy to have within WordPress so that updated parts automatically initiate cache removal. There are several plugins for that, we use Better WP Varnish, but any will do.

The second thing to mention is cookie handling. This configuration prevents logged in users to see cached versions of WordPress for easier debugging and previewing.

Last, we also use a custom header (X-Cache), which equals to either HIT or MISS so you can easily debug and see whether Varnish fetched a cached version (HIT) or not.

As with Pound, it’s now time to restart Varnish to load the new configuration: $ service varnish restart.

Nginx and mod_pagespeed

Almost there! The final piece of the puzzle is Nginx. Because we didn’t use the package manager to install it, we first need to set up an init script that will register Nginx as a daemon (a program or a process that runs in the background). In order to do that, run the following commands:

[code lang=text]
$ cd /etc/init.d
$ wget https://raw.githubusercontent.com/JasonGiedymin/nginx-init-ubuntu/master/nginx
$ chmod +x nginx
$ update-rc.d nginx defaults
[/code]

This downloads the init script, makes it executable (because it’s essentially a program) and registers it with the operating system so it can start at boot time.

Once that’s sorted, we need to configure Nginx by editing the file /usr/local/nginx/conf/nginx.conf. Locate the first server block in the file, and replace it with the following code (this assumes you have your WordPress installed in /srv/www/yourdomain.com):

[code lang=text]
server {
listen 8080;
server_name test.com;

root /srv/test.com;
index index.php index.html;

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

location ~ .php$ {
try_files $uri /index.php;
include fastcgi_params;
fastcgi_pass unix:/var/run/php5-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_index index.php;
}

pagespeed On;
pagespeed ModifyCachingHeaders on;

# Needs to exist and be writable by nginx.
pagespeed FileCachePath "/var/cache/ngx_pagespeed/";
pagespeed LoadFromFile "https://yourdomain.com" "/srv/yourdomain.com";
pagespeed MapOriginDomain "http://localhost" "https://yourdomain.com";
pagespeed EnableFilters rewrite_css,combine_css,trim_urls;
pagespeed DisableFilters sprite_images,convert_jpeg_to_webp,convert_to_webp_lossless;

}
[/code]

The top half is just basic configuration for our virtual host which enables rewrites and delegates PHP processing to PHP-FPM (listening on a socket).

What’s interesting here is the pagespeed part. After turning it on, we need to properly map URLs so that mod_pagespeed will know when to trigger its magic. After that, we enable/disable various filters and it’s up to you to which level you want mod_pagespeed to modify/optimize your HTML so go ahead and play with them. My favorites are ones that automatically combine CSS and Javascript files, optimize images and rename them according to their last-modified time, so we can have a far-future expiration date.

There’s only one caveat: Make sure to disable convert_jpeg_to_webp filter. The name of the filter is pretty descriptive but can pose a problem because of Varnish. If the very first visitor of your website uses Chrome, Varnish will pass the request on to Nginx and it will detect that and serve images in WEBP format, filling the cache with them. All subsequent users will then get that cached version, but as WEBP is not yet widely supported users with other browsers won’t be able to see the images!

Now it’s time to see if our effort was fruitful; Restart Nginx to load the new configuration ($ service nginx restart) and visit the website. UH-OH! You probably don’t see what you expected, right? That’s because Varnish is working, and probably serving erroneous version in case you visited it before. The fix is simple, just clear all Varnish cache ($ varnishadm "ban req.url ~ /") and you should be able to see the proper output.

To verify, you might want to inspect the response locally: $ curl -i https://yourdomain.com. In the response you should see two headers: X-Cache and X-Page-Speed, which indicates you’re now a webmaster of a considerably faster WordPress website!

What about benchmarks?

I’ve been asked about benchmarking the results, but in this case, I don’t believe it would make much sense. There’s plenty of Varnish benchmarks out there, but mod_pagespeed isn’t just about speed (despite it’s name). As per it’s documentation it can do a whole lot to optimize your website output in terms of asset concatenation, minification and serving.

That being said, if you still prefer having hard numbers for this, feel free to test it out and report back and I’ll be more than happy to update this article with your findings.

Conclusion

What I didn’t address in this article is WordPress installation, GZIPping, error handling and various possible corner cases, but I think this tutorial should give you a good base to start off with on the path to improved user experience.