Configure WordPress Varnish 4 Cache with Apache or nginx

varnish-cache-logo-text-200Varnish is a refresh proxy that serves your WordPress site super fast. It will cache your WordPress site as compiled html pages so users avoid the web server (Apache2 and nginx) and slow PHP. It is very easy to set up WordPress Varnish cache on your VPS or dedicated server.

This guide is for Debian Jessie but Wheezy will work, so too will Ubuntu and Raspberry Pi, Banana Pi devices. You must be on a 64-bit machine to use Varnish 4's repository, however Debian 8 Jessie 32-bit has Varnish 4 in its repository. The only limitation I have experienced so far using Varnish is using a custom renamed login page with Apache 2 which can now be fixed by adding exceptions i backend_fetch. but I recommend protecting your login page with a free CloudFlare account.

Varnish gives you the fastest load times that a regular WordPress PHP backend alone has difficulty competing with a VPS like Digital Ocean ($5 a month). Bear in mind this site would take over 3 seconds to load because I have a lot of plugins (40+).

VPS Provider
Locations
RAM
Hard Drive
Speed
Price
Vultr
US, EU, Asia
768 MB
15 GB SSD
100 Mbps
$5 / month
Digital Ocean
US, EU, Asia
512 MB
20 GB SSD
100 Mbps
$5 / month
HostUS
US, UK, China, Australia
768 MB
20 GB
1-10 Gbps
$15 / year

If you want to install Varnish 5 see this guide.

I suggest you take speed tests using pingdom now and compare them to your results post Varnish configuration.

Configure WordPress Varnish 4 Cache with Apache or nginx

The diagram on the left shows the traditonal server setup. nginx or Apache listens on port 80. On the right is the new setup, Varnish will listen on port 80 and you will change your web server to listen on port 8080.

Configure Web Server to Use Varnish

Configure Apache for Varnish Cache

Open your Apache configuration file

sudo nano /etc/apache2/ports.conf

Change these lines so Apache listens on port 8080 instead

NameVirtualHost *:8080
Listen 8080

Ctrl+X, Y and Enter to save

Now adjust your Apache virtual host files for WordPress

sudo nano /etc/apache2/sites-available/wordpress

Change this line to match so the virtual host listens on port 8080

<VirtualHost 127.0.0.1:8080>

Ctrl+X, Y and Enter to save. Do not restart Apache yet

Configure nginx for Varnish Cache

Make sure you have disabled the default virtual host

sudo unlink /etc/nginx/sites-enabled/default

Open your nginx virtual host file, it may not be called wordpress

sudo nano /etc/nginx/sites-available/wordpress

In your server block change the port from 80 to 8080

server {
        server_name www.htpcguides.com htpcguides.com;
        listen 8080;

Ctrl+X, Y and Enter to save

To get the Real IP of your WordPress user comments and emails from Varnish, use the Real IP feature of nginx

sudo nano /etc/nginx/conf.d/cloudflare.conf

Paste this to get the Real IP from CloudFlare and Varnish, we are using the X-Actual IP created earlier

#CloudFlare
set_real_ip_from   199.27.128.0/21;
set_real_ip_from   173.245.48.0/20;
set_real_ip_from   103.21.244.0/22;
set_real_ip_from   103.22.200.0/22;
set_real_ip_from   103.31.4.0/22;
set_real_ip_from   141.101.64.0/18;
set_real_ip_from   108.162.192.0/18;
set_real_ip_from   190.93.240.0/20;
set_real_ip_from   188.114.96.0/20;
set_real_ip_from   197.234.240.0/22;
set_real_ip_from   198.41.128.0/17;
set_real_ip_from   162.158.0.0/15;
set_real_ip_from   104.16.0.0/12;
set_real_ip_from   172.64.0.0/13;
set_real_ip_from   2400:cb00::/32;
set_real_ip_from   2606:4700::/32;
set_real_ip_from   2803:f800::/32;
set_real_ip_from   2405:b500::/32;
set_real_ip_from   2405:8100::/32;

#For use with Varnish
set_real_ip_from   127.0.0.1/32;
real_ip_header     X-Actual-IP;

Ctrl+X, Y and Enter to save the WordPress nginx real IP configuration

We will restart nginx later

Install Varnish 4 Cache

Add the Varnish 4 Cache repository, you may need to adjust jessie to wheezy if you are not on Jessie yet. If using Ubuntu you can change jessie to precise (12.04) or trusty (14.04 and later).

Remember you need to be on a 64-bit distro to get the Varnish 4 binary from the repository, 32-bit users should use the next instructions.

sudo apt-get install apt-transport-https -y
wget -O - https://repo.varnish-cache.org/GPG-key.txt | sudo apt-key add -
echo "deb https://repo.varnish-cache.org/debian/ jessie varnish-4.1" >> /etc/apt/sources.list.d/varnish-cache.list
echo "deb-src https://repo.varnish-cache.org/debian/ jessie varnish-4.1" >> /etc/apt/sources.list.d/varnish-cache.list
sudo apt-get update
sudo apt-get install varnish -y

Debian 8 Jessie has Varnish 4.0.2 in its repository

sudo apt-get update
sudo apt-get install varnish -y

If you are using systemd the file /lib/systemd/system/varnish.service will exist, change the Varnish systemd service to use port 80

sudo mkdir -p /etc/systemd/system/varnish.service.d
sudo nano /etc/systemd/system/varnish.service.d/local.conf

Paste these lines (thanks to the Arch page)

[Service]
ExecStart=
ExecStart=/usr/sbin/varnishd -a :80 -T localhost:6082 -f /etc/varnish/default.vcl -S /etc/varnish/secret -s malloc,256m

Ctrl+X, Y and Enter to save

If you do not have the Varnish systemd script then you are probably using an init.d script in which case change 6081 to 80 in this file

sudo nano /etc/default/varnish

Back up the default Varnish 4 vcl file

mv /etc/varnish/default.vcl /etc/varnish/default.vcl.bak

Create your new WordPress Varnish 4 cache vcl file

sudo nano /etc/varnish/default.vcl

Paste the vcl adapted from here, replace the Web.Server.IP with the IP of your server (can get your IP with ifconfig). The purge acl is used to verify curl requests to refresh the cache and smart refresh the Varnish cache using hash_always_miss from your web server are the only IPs that can refresh or purge the Varnish cache. This vcl is designed for use with the smart refresh scripts and behind CloudFlare.

/* SET THE HOST AND PORT OF WORDPRESS
 * *********************************************************/
vcl 4.0;
import std;

backend default {
  .host = "127.0.0.1";
  .port = "8080";
  .first_byte_timeout = 60s;
  .connect_timeout = 300s;
}
 
# SET THE ALLOWED IP OF PURGE REQUESTS
# ##########################################################
acl purge {
  "localhost";
  "127.0.0.1";
  "Web.Server.IP";
}

#THE RECV FUNCTION
# ##########################################################
sub vcl_recv {

# set realIP by trimming CloudFlare IP which will be used for various checks
set req.http.X-Actual-IP = regsub(req.http.X-Forwarded-For, "[, ].*$", ""); 

        # FORWARD THE IP OF THE REQUEST
  if (req.restarts == 0) {
    if (req.http.x-forwarded-for) {
      set req.http.X-Forwarded-For =
      req.http.X-Forwarded-For + ", " + client.ip;
    } else {
      set req.http.X-Forwarded-For = client.ip;
    }
  }

 # Purge request check sections for hash_always_miss, purge and ban
 # BLOCK IF NOT IP is not in purge acl
 # ##########################################################

  # Enable smart refreshing using hash_always_miss
if (req.http.Cache-Control ~ "no-cache") {
    if (client.ip ~ purge || std.ip(req.http.X-Actual-IP, "1.2.3.4") ~ purge) {
         set req.hash_always_miss = true;
    }
}

if (req.method == "PURGE") {
    if (!client.ip ~ purge || !std.ip(req.http.X-Actual-IP, "1.2.3.4") ~ purge) {
        return(synth(405,"Not allowed."));
        }
    return (purge);
  }

if (req.method == "BAN") {
        # Same ACL check as above:
        if (!client.ip ~ purge || !std.ip(req.http.X-Actual-IP, "1.2.3.4") ~ purge) {
                        return(synth(403, "Not allowed."));
        }
        ban("req.http.host == " + req.http.host +
                  " && req.url == " + req.url);

        # Throw a synthetic page so the
        # request won't go to the backend.
        return(synth(200, "Ban added"));
}

# Unset cloudflare cookies
# Remove has_js and CloudFlare/Google Analytics __* cookies.
      set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_[_a-z]+|has_js)=[^;]*", "");
      # Remove a ";" prefix, if present.
     set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");

  # For Testing: If you want to test with Varnish passing (not caching) uncomment
  # return( pass );

# DO NOT CACHE RSS FEED
 if (req.url ~ "/feed(/)?") {
	return ( pass ); 
}

#Pass wp-cron

if (req.url ~ "wp-cron\.php.*") {
 return ( pass );
}

## Do not cache search results, comment these 3 lines if you do want to cache them

if (req.url ~ "/\?s\=") {
	return ( pass ); 
}

# CLEAN UP THE ENCODING HEADER.
  # SET TO GZIP, DEFLATE, OR REMOVE ENTIRELY.  WITH VARY ACCEPT-ENCODING
  # VARNISH WILL CREATE SEPARATE CACHES FOR EACH
  # DO NOT ACCEPT-ENCODING IMAGES, ZIPPED FILES, AUDIO, ETC.
  # ##########################################################
  if (req.http.Accept-Encoding) {
    if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
      # No point in compressing these
      unset req.http.Accept-Encoding;
    } elsif (req.http.Accept-Encoding ~ "gzip") {
      set req.http.Accept-Encoding = "gzip";
    } elsif (req.http.Accept-Encoding ~ "deflate") {
      set req.http.Accept-Encoding = "deflate";
    } else {
      # unknown algorithm
      unset req.http.Accept-Encoding;
    }
  }

  # PIPE ALL NON-STANDARD REQUESTS
  # ##########################################################
  if (req.method != "GET" &&
    req.method != "HEAD" &&
    req.method != "PUT" && 
    req.method != "POST" &&
    req.method != "TRACE" &&
    req.method != "OPTIONS" &&
    req.method != "DELETE") {
      return (pipe);
  }
   
  # ONLY CACHE GET AND HEAD REQUESTS
  # ##########################################################
  if (req.method != "GET" && req.method != "HEAD") {
    return (pass);
  }
  
  # OPTIONAL: DO NOT CACHE LOGGED IN USERS (THIS OCCURS IN FETCH TOO, EITHER
  # COMMENT OR UNCOMMENT BOTH
  # ##########################################################
  if ( req.http.cookie ~ "wordpress_logged_in|resetpass" ) {
    return( pass );
  }

  #fix CloudFlare Mixed Content with Flexible SSL
  if (req.http.X-Forwarded-Proto) {
    return(hash);
  }

  # IF THE REQUEST IS NOT FOR A PREVIEW, WP-ADMIN OR WP-LOGIN
  # THEN UNSET THE COOKIES
  # ##########################################################
  if (!(req.url ~ "wp-(login|admin)") 
    && !(req.url ~ "&preview=true" ) 
  ){
    unset req.http.cookie;
  }

  # IF BASIC AUTH IS ON THEN DO NOT CACHE
  # ##########################################################
  if (req.http.Authorization || req.http.Cookie) {
    return (pass);
  }
  
  # IF YOU GET HERE THEN THIS REQUEST SHOULD BE CACHED
  # ##########################################################
  return (hash);
  # This is for phpmyadmin
if (req.http.Host == "pmadomain.com") {
return (pass);
}
}

sub vcl_hash {

if (req.http.X-Forwarded-Proto) {
    hash_data(req.http.X-Forwarded-Proto);
    }
}


# HIT FUNCTION
# ##########################################################
sub vcl_hit {
  return (deliver);
}

# MISS FUNCTION
# ##########################################################
sub vcl_miss {
  return (fetch);
}

# FETCH FUNCTION
# ##########################################################
sub vcl_backend_response {
  # I SET THE VARY TO ACCEPT-ENCODING, THIS OVERRIDES W3TC 
  # TENDANCY TO SET VARY USER-AGENT.  YOU MAY OR MAY NOT WANT
  # TO DO THIS
  # ##########################################################
  set beresp.http.Vary = "Accept-Encoding";

  # IF NOT WP-ADMIN THEN UNSET COOKIES AND SET THE AMOUNT OF 
  # TIME THIS PAGE WILL STAY CACHED (TTL), add other locations or subdomains you do not want to cache here in case they set cookies
  # ##########################################################
  if (!(bereq.url ~ "wp-(login|admin)") && !bereq.http.cookie ~ "wordpress_logged_in|resetpass" ) {
    unset beresp.http.set-cookie;
    set beresp.ttl = 1w;
    set beresp.grace =3d;
  }

  if (beresp.ttl <= 0s ||
    beresp.http.Set-Cookie ||
    beresp.http.Vary == "*") {
      set beresp.ttl = 120 s;
      # set beresp.ttl = 120s;
      set beresp.uncacheable = true;
      return (deliver);
  }

  return (deliver);
}

# DELIVER FUNCTION
# ##########################################################
sub vcl_deliver {
  # IF THIS PAGE IS ALREADY CACHED THEN RETURN A 'HIT' TEXT 
  # IN THE HEADER (GREAT FOR DEBUGGING)
  # ##########################################################
  if (obj.hits > 0) {
    set resp.http.X-Cache = "HIT";
  # IF THIS IS A MISS RETURN THAT IN THE HEADER
  # ##########################################################
  } else {
    set resp.http.X-Cache = "MISS";
  }
}

Ctrl+X, Y and Enter to Save

Reload, enable and restart the Varnish systemd service, restart the nginx service before Varnish

sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo service nginx restart
sudo service varnish restart

Reload, enable and restart the Varnish systemd service, restart the Apache service before Varnish

sudo systemctl daemon-reload
sudo systemctl enable varnish
sudo service apache2 restart
sudo service varnish restart

Lastly, to fix any compatibility issues with WordPress Varnish plugins add your hostname to your VPS hosts file

sudo nano /etc/hosts

Optionally, add your host to the loopback address 127.0.0.1 and also for your VPS public IP.

Warning this could cause issues with postfix sending email.

127.0.0.1 localhost.localdomain localhost htpcguides.com www.htpcguides.com

Ctrl+X, Y and Enter to Save

You can test Varnish by going to Is Varnish Working?

Consider setting up Varnish DDoS protection and the Varnish Web Application Firewall to prevent WordPress SQL injection and XSS attacks is in development.