Install + Configure Varnish 3 Cache with nginx for WooCommerce Speed

Varnish is one of the best WooCommerce caching solutions I have tried. I've managed to get WooCommerce shop load times under 1 second! The Varnish control language (vcl) gives you incredible control over what should be cached and how, providing greater flexibility than with nginx proxy or fastcgi cache systems.

Using Varnish will give your web server running nginx, PHP and MariaDB or MySQL some relief so that they will only be used when customers are actually adding products to their shopping cart. Window shoppers will only be using Varnish for browsing providing awesome speed.

woocommerce-varnish-cache

Install + Configure Varnish 3 Cache with nginx for WooCommerce Speed

Installation Overview

  • Configure nginx running WooCommerce to use Varnish
  • Install Varnish 3 Cache
  • Configure Varnish Cache for WooCommerce

Configure WooCommerce to Use Varnish 3 Cache

Varnish will listen on port 80 and you will change nginx to listen on port 8080

Configure nginx for Varnish Cache

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

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

In your server block change the port from 80 to 8080, you will need to do this for all of your nginx virtual hosts.

server {
        server_name www.wp-bullet.com wp-bullet.com;
        listen 8080;

Ctrl+X, Y and Enter to save

To get the Real IP of your WooCommerce 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 of your WooCommerce customers from CloudFlare and Varnish, we are using the X-Actual IP in the default.vcl created later on.

#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.

Test the nginx virtual host configuration is valid.

sudo nginx -t

We will restart nginx later.

Install Varnish 3 Cache

Add the Varnish repository for Debian, you may need to adjust wheezy to jessie if you have upgraded to Debian Jessie.

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/ wheezy varnish-3.0" | sudo tee -a /etc/apt/sources.list.d/varnish-cache.list
echo "deb-src https://repo.varnish-cache.org/debian/ wheezy varnish-3.0" | sudo tee -a /etc/apt/sources.list.d/varnish-cache.list
sudo apt-get update
sudo apt-get install varnish -y

If you are on Ubuntu 14.04 LTS then use these steps

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/ubuntu/ trusty varnish-3.0" | sudo tee -a /etc/apt/sources.list.d/varnish-cache.list
echo "deb-src https://repo.varnish-cache.org/debian/ trusty varnish-3.0" | sudo tee -a /etc/apt/sources.list.d/varnish-cache.list
sudo apt-get update
sudo apt-get install varnish -y

Enable Varnish 3 Cache to listen on port 80 and start the daemon with the init.d startup script with these steps.

If you are on Debian Jessie you will need to update the Varnish systemd script outlined here, return to this page for the Varnish 3 vcl.

sudo nano /etc/default/varnish

Change this line to enable the Varnish daemon

START=yes

Then scroll down to Alternative 2 and change 6081 to 80 so Varnish will listen on port 80

DAEMON_OPTS="-a :80 \
             -T localhost:6082 \
             -f /etc/varnish/default.vcl \
             -S /etc/varnish/secret \
             -s malloc,256m"

Configure Varnish 3 Cache for WooCommerce

Now back up the Varnish vcl file which contains all of the rules for treating requests and URLs

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

Create the new Varnish vcl rules configuration file

sudo nano /etc/varnish/default.vcl

This Varnish 3 vcl was adapted from here and here to be optimized for WooCommerce.

Paste this WooCommerce Varnish vcl configuration, replace Web.Server.IP with your web server's IP

/* SET THE HOST AND PORT OF WooCommerce
 * *********************************************************/

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

#THE RECV FUNCTION
# ##########################################################
sub vcl_recv {
	#remove HTTPOXY CGI vulnerability
	unset req.http.proxy;

	#remove extraneous host ports
	set req.http.host = regsub(req.http.Host, ":[0-9]+", "");

	# 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, "[, ].*$", ""); 

	# Enable smart refreshing
	if (req.http.Cache-Control ~ "no-cache" && client.ip ~ purge) {
		set req.hash_always_miss = true;
	}

	# 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 );

	# 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;
		}
	}

# DO NOT CACHE RSS FEED
	if (req.url ~ "/feed/") {
		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
			remove 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
		remove req.http.Accept-Encoding;
		}
	}

# IF THIS IS A PURGE REQUEST, THEN CHECK THE IPS SET ABOVE
# BLOCK IF NOT ONE OF THOSE IPS
# ##########################################################
	if (req.request == "PURGE") {
		if ( !client.ip ~ purge ) {
			error 405 "Not allowed.";
		}
		return (lookup);
	}

# PIPE ALL NON-STANDARD REQUESTS
# ##########################################################
	if (req.request != "GET" &&
		req.request != "HEAD" &&
		req.request != "PUT" && 
		req.request != "POST" &&
		req.request != "TRACE" &&
		req.request != "OPTIONS" &&
		req.request != "DELETE") {
		return (pipe);
	}
   
# ONLY CACHE GET AND HEAD REQUESTS
# ##########################################################
	if (req.request != "GET" && req.request != "HEAD") {
		return (pass);
	}
  
# OPTIONAL: DO NOT CACHE LOGGED IN USERS and CARTS
# ##########################################################
	if ( req.http.cookie ~ "wordpress_logged_in|resetpass" ) {
		return( pass );
	}
  
	if (req.url ~ "/wp-(login|admin|cron)|wc-api|cart|my-account|checkout|addons|administrator|resetpass|\?wc-ajax=get_refreshed_fragments") {
		# Don't cache, pass to backend
		return (pass);
	}

	if ( req.url ~ "\?add-to-cart=" ) {
		return (pass);
	}
	
#fixed non AJAX cart problem, may need to add wp_woocommerce_session_
	if (req.http.cookie ~ "woocommerce_(cart|session)") {
		return(lookup);
	}

	if (!req.url ~ "/wp-(login|admin|cron)|wc-api|cart|my-account|checkout|addons|administrator|resetpass") {
		# Don't cache, pass to backend
		unset req.http.cookie;
	}

# This is for phpmyadmin
	if (req.http.Host == "pmadomain.com") {
		return (pass);
	}
	
# IF YOU GET HERE THEN THIS REQUEST SHOULD BE CACHED
# ##########################################################
	return (lookup);
}

sub vcl_hash {
#this is to store cache based on PHPSESSID or woocommerce cookie so cart doesn't show 0
	if (req.http.cookie) {
		hash_data(req.http.cookie);
	}
    #fix flexible ssl css
	if (req.http.x-forwarded-proto) {
		hash_data(req.http.x-forwarded-proto);
	}
}

# HIT FUNCTION
# ##########################################################
sub vcl_hit {
# IF THIS IS A PURGE REQUEST THEN DO THE PURGE
# ##########################################################
	if (req.request == "PURGE") {
		purge;
		error 200 "Purged.";
	}
	
	return (deliver);
}

# MISS FUNCTION
# ##########################################################
sub vcl_miss {
	if (req.request == "PURGE") {
		purge;
		error 200 "Purged.";
	}
	
	return (fetch);
}

# FETCH FUNCTION
# ##########################################################
sub vcl_fetch {
# 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";

# You may need to add other locations like membership sites here, 302 is necessary if you use redirect to cart
# ##########################################################
	if (!(req.url ~ "wp-(login|admin)|wc-api|resetpass|cart|checkout|my-account|\?wc-ajax=get_refreshed_fragments") &&
            !req.http.cookie ~ "wordpress_logged_in|resetpass" &&
            !beresp.status == 302) {
		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;
		return (hit_for_pass);
	}
	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

Test the Varnish vcl syntax is valid

sudo varnishd -C -f /etc/varnish/default.vcl

If you got no errors then you can restart nginx and Varnish

sudo service nginx restart && sudo service varnish restart

Enjoy your super fast WooCommerce site powered by Varnish cache.

Sources