fbpx

NGINX / Varnish / Wordpress

Accelerate WordPress with Varnish Cache

by , , revisited on


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

WordPress is a widely used content management system, but it can often be slow to load pages, especially when dealing with high traffic. Fortunately, there is a solution to this problem – Varnish.

Varnish is a powerful caching software that can significantly speed up your WordPress site by providing a full page cache (FPC) of its pages and delivering them quickly to your users.

In this article, we will show you how to accelerate WordPress with Varnish on a RedHat-based system. We will assume that your WordPress site is hosted on NGINX.

How Varnish excels in comparison to WordPress caching plugins

It is important to understand the many benefits of Varnish that make WordPress caching plugins fade in comparison:

  • Varnish caches complete pages and serves cached pages without the use of PHP. WordPress cache plugins rely on running PHP to deliver cached pages, which is slow. You can configure many of the WordPress cache plugins to serve cached pages via NGINX, but this is still slow because it relies on checking files on the disk
  • Varnish uses RAM as its cache storage by default. A cached page is delivered without touching the disk at all!
  • Varnish has an amazing VCL configuration language that allows you to be very flexible in your cache configuration rules.

Step 1: Remove an existing caching plugin

If you have an existing full-page cache plugin installed on your WordPress site, you will need to remove it. Varnish is far more effective than any WordPress caching plugin, and using both can cause conflicts and unexpected behavior.

Note that this does not apply to object cache plugins. Object cache plugins will nicely complement your Varnish-based full-page cache.

Step 2: Install Varnish

The next step is to install Varnish on your server.
At this time, we recommend fetching Varnish 6.0.x LTS using our repository. For example, on a CentOS/RHEL 7 system, you can run:

sudo yum -y install https://extras.getpagespeed.com/release-latest.rpm yum-utils
sudo yum-config-manager --enable getpagespeed-extras-varnish60
sudo yum -y install varnish

In case you want to rely on whichever Varnish version is shipped with your operating system, use the following command:

sudo yum install varnish

Once Varnish is installed, you can enable its automatic startup and launch immediately using the following command:

sudo systemctl enable --now varnish

By default, Varnish listens on port 6081. You don’t need to change its port. We are going to use NGINX as a TLS terminator. It will forward requests to Varnish, and we can keep using NGINX for HTTP to HTTPS redirects.

Step 3. Configure Varnish

Varnish configuration requires modifying /etc/varnish/default.vcl. What we are going to do there, is define which server Varnish has to talk to in order to fetch content when Varnish receives a request. That is our “main” NGINX server block.
And we will also configure how Varnish handles cache purge requests.

Here’s a sample Varnish configuration that will suffice for a WordPress installation that doesn’t have cache-unfriendly plugins (read on those below):

vcl 4.0;

# Where Varnish should forward requests
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

# Which IP addresses can send cache purge requests
acl purge {
    "localhost";
    "127.0.0.1";
    "::1";
}
sub vcl_recv {   
    # Remove the proxy header to mitigate the httpoxy vulnerability
    # See https://httpoxy.org/
    unset req.http.proxy;

    # Purge logic
    if(req.method == "PURGE") {
        if(!client.ip ~ purge) {
            return(synth(405,"PURGE not allowed for this IP address"));
        }
        if (req.http.X-Purge-Method == "regex") {
            ban("obj.http.x-url ~ " + req.url + " && obj.http.x-host == " + req.http.host);
            return(synth(200, "Purged"));
        }
        ban("obj.http.x-url == " + req.url + " && obj.http.x-host == " + req.http.host);
        return(synth(200, "Purged"));
    }
}

sub vcl_backend_response {
    # Inject URL & Host header into the object for asynchronous banning purposes
    set beresp.http.x-url = bereq.url;
    set beresp.http.x-host = bereq.http.host;
}

sub vcl_deliver {
    # Cleanup of headers
    unset resp.http.x-url;
    unset resp.http.x-host;    
}

Now you can reload the Varnish configuration by restarting it:

systemctl restart varnish

Step 4: Install required WordPress plugins

Varnish caches the latest versions of your WordPress pages, and you will need to install a plugin that can automatically invalidate cached pages when changes are made. We recommend using the Proxy Cache Purge plugin.

Aside from that, WordPress can be enabled for better Varnish compatibility with the Cacheability plugin. It will instruct it to cache things longer with the right Cache-Control header.

Install and activate the plugins using WordPress CLI:

wp plugin install cacheability varnish-http-purge --activate

Once the plugins are installed, you can configure WordPress with the Varnish IP and port:

wp option add vhp_varnish_ip 127.0.0.1:6081

Step 5: Configure NGINX

The next step is to configure NGINX to work with Varnish. We will set up NGINX as a “sandwich” with the first NGINX server acting as a TLS terminator and the second NGINX server forwarding requests to Varnish.

To do this, we will need to make some changes to the existing NGINX configuration file. For simplicity of explanation, we assume your current NGINX setup consists of 2 server blocks. One block does HTTP to HTTPS redirection and the other block actually handles PHP requests, aka the main server block:

# the redirection server block:
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# the main server block:
server {
    listen 443 ssl http2;
    server_name example.com;
    # SSL configuration goes here
    # ...
    location ~ .php$ {
        fastcgi_pass ...
    }
}

When we introduce Varnish as a caching layer, we have to make these changes:

  • adjust the main server block to listen on an arbitrary port, e.g. 8080
  • introduce the TLS termination server block

The first change is rather trivial. Under your main server block, remove the SSL configuration and change listen 443 ssl; to listen 8080;:

# the main server block:
server {
    listen 8080;
    server_name example.com;
    # SSL configuration goes here
    # ...
    location ~ .php$ {
        fastcgi_pass ...
    }
}

Next up, add a new server {} TLS terminating block like this:

upstream varnish {
  server 127.0.0.1:6081;
  keepalive 64;
}

server {
    listen 443 ssl http2;
    # SSL configuration goes here
    # ...
    server_name example.com;
    location / {
        proxy_pass http://varnish;
        proxy_buffering off;
        proxy_request_buffering off;
        proxy_set_header X-Real-IP  $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header Host $host;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header X-Forwarded-Host $http_host;
        proxy_set_header Ssl-Offloaded "1";
    }
}

Next, we will use NGINX real IP module to ensure that NGINX trusts the end visitor IP address from Varnish, and does not include the Varnish port while performing redirects.
Create /etc/nginx/conf.d/varnish.conf with contents:

set_real_ip_from 127.0.0.1;
port_in_redirect off;
absolute_redirect off;

Another change required in your NGINX configuration, is letting PHP know about our actual secure layer on the front.
To do that, locate your PHP handler block, and modify it like so:

location ~ .php$ {
    include fastcgi_params;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param HTTPS on;  # <-- add this
    fastcgi_param SERVER_PORT 443;  # <-- add this
    fastcgi_pass unix:/path/to/your/php-fpm.sock;
}

This is required, because WordPress does not support reading the X-Forwarded-Proto, and by setting these FastCGI parameters explicitly, WordPress will know it’s running over `https://`,
and will not make unnecessary redirects.

So now you can reload NGINX configuration by running systemctl reload nginx.
Any request coming to the site will be redirected to HTTPS, which is routed via Varnish as the caching layer.

Step 6: Test and monitor

With Varnish and NGINX configured, you should notice a significant improvement in the speed of your WordPress site. However, it’s important to monitor your site and test it regularly to ensure that everything is working as expected.

You can use tools like Pingdom or GTmetrix to test the load time of your site and identify any areas that may still be slow. Additionally, you can use the Varnish log files to monitor traffic and identify any issues.

To easily check if your website is being cached fine in Varnish, examine its response headers and look for the X-Varnish or Age header:

  • The Age header tells you how long Varnish has the page in the cache. A positive value indicates a cache hit, while 0 means a cache miss.
  • The X-Varnish include one or two numbers. The first number is the incoming request ID and the second number is the response ID. Two numbers present in the header mean the request is a cached one. And a single number indicates a cache miss.

Improving your Varnish cache hit ratio

Here is a couple of reasons why you’re may not getting a cache hit in Varnish and how to correct them.

1. Cache-unfriendly plugins

What about cache-unfriendly plugins? If your cache doesn’t work, you probably have one. There is a myriad of plugins that have no regard for external caches like Varnish. Most of the time they are badly coded and issues have to be filed with the plugins’ authors.

The number one bad thing that those plugins do is: start a PHP session without any good reason.
A well-written plugin should start a PHP session when there’s data to persist to the browser and never again.

To check if you have such a plugin, examine the response headers of your website and look for Set-Cookie.
If Varnish sees such a response from WordPress, such a page can’t be cached, as Varnish simply ensures that user-specific data (which the cookie is) isn’t shared between users.

2. Tracking cookies

If you use Google Analytics or any other kind of tracking for your website, chances are that they rely on cookies to persist their data.
When a cookie is set for your website, the browser will send it with every subsequent request.

It is important to understand that Varnish will by default, bypass its cache whenever it sees any cookie in the request. This isn’t Varnish limitation. It simply does this by default to prevent user-specific information being shared between users.

How to deal with this and improve cacheability?

Certainly, you can configure some complex logic to cache user sessions separately, but we’re going to mention a more simple solution here that will be sufficient for many websites.

The solution is whitelisting WordPress cookies in your VCL config and stripping any other cookies. This can be done by adding the following in your vcl_recv { ... } block:

    if (req.http.Cookie !~ "comment_author_|wordpress_(?!test_cookie)|wp-postpass_|woocommerce") {
        # no essential cookies, nuke the rest!
        unset req.http.cookie;
    }

Don’t forget to apply the updated config by running systemctl reload varnish.

3. Unnecessary query parameters

If you use AMP with your WordPress, chances are that the AMP pages include special noamp URLs in the footer. These are actually absolutely identical to the standard pages of your website, and differ only by URL ending with ?noamp=mobile. To improve your cacheability, you may want to add this to your vcl_recv section:

    # Strip "?noamp=mobile" from URL
    set req.url = regsub(req.url, "?noamp=mobile$", "");

Warm your cache

A page that has not yet been visited is not cached. To improve user experience you may want to warm the cache to ensure that visitors get cached content only.

The Proxy Cache Purge plugin works by purging pages from cache whenever you update your page. This ensures that the visitors get to see the fresh data.
Our complementary Proxy Cache Warmer plugin automatically warms the purged pages, so even if you’re actively updating your content, visitors are experiencing faster cached browsing.

Conclusion

Varnish is an incredibly powerful caching system that can significantly speed up your WordPress site. By configuring NGINX as a TLS terminator and setting up Varnish to cache frequently accessed pages, you can ensure that your site is fast and responsive for your users.

The article by no means covers all the specifics of configuring Varnish with WordPress. We intentionally made a few assumptions in order to illustrate how Varnish and NGINX can be coupled together. Hopefully, this lets you get started on your journey to a well-behaving, well-cached WordPress installation. For more details, you may want to check out our blog posts.

  1. Tim

    Thank you for this wonderful interesting article! I have a question regarding caching: at the moment I use nginx as reverse proxy with WordPress and PHP FPM. I usr nginx fastcgi cache and would like to ask, if varnish provides a benefit over the nginx built in caching capabilities?
    Thanks and greetings
    Tim

    Reply
    • Danila Vershinin

      Hi Tim,

      If you configure the NGINX FastCGI cache to be stored in memory, set a decently large cache lifetime value, and set up cache purging from within your app (e.g. by using it together with a module like cache-purge, it will perform just as good as Varnish and you’ll benefit from reduced software overhead.

      However, the main benefit of Varnish still prevails: its VCL configuration language allows you to configure caching in virtually unlimited ways, while with NGINX declarative configuration nature, complicated caching rules will be written with numerous maps and simply will be a pain to maintain. And of course, Varnish supports ESI for block-based caching, something that NGINX unfortunately doesn’t support.

      Reply
  2. DF

    Thanks for sharing your post! Unfortunately, I encountered an issue with my WordPress site after enabling Varnish. It appears that WordPress is redirecting to the HTTPS version URL while Varnish only speaks HTTP. Could you please provide some advice on how to resolve this issue? Thank you!

    Reply
    • Danila Vershinin

      You might have missed the part with “add a new server {} TLS terminating block like this”…

      Reply
      • DF

        I haven’t missed configuring that server block. Actually, I found a solution to this issue. Just set the PHP fastcgi protocol parameter to https on. 😊

        Reply
        • Danila Vershinin

          Thank you, you’re right. Alas, WordPress doesn’t read the X-Forwarded-Proto header so it is indeed required to override the HTTPS FastCGI parameter.

          Reply
  3. Kevin

    Thank you for awesome tutorial. However, I got the issue with WooCommerce cookies like woocommerce_recently_viewed.

    I tried to unset with functions.php on child theme,

    unset($_COOKIE[‘woocommerce_recently_viewed’]);

    I even used varnish to remove that cookies but the same issue. Is there something wrong with proxy?

    if (req.http.Cookie) {
    cookie.parse(req.http.Cookie);
    cookie.delete(“woocommerce_recently_viewed”);
    set req.http.Cookie = cookie.get_string();
    }

    Reply
    • Danila Vershinin

      This cookie will be quite problematic in regard to caching. Most likely the code of the theme/plugin sets it on every page. This will result in Set-Cookie with a negative cache lifetime sent by PHP and when Varnish sees it – no caching. The best way around is to disable that functionality if possible so that the cookie won’t be sent or received later.

      If this function is not critical, disabling this way should work (in functions.php):

      function disable_woocommerce_recently_viewed_cookie() {
          if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) {
              remove_action( 'wp_loaded', array( WC()->query, 'track_recently_viewed_products' ) );
              remove_action( 'wc_ajax_get_refreshed_fragments', array( WC()->query, 'track_recently_viewed_products' ) );
          }
      }
      add_action( 'init', 'disable_woocommerce_recently_viewed_cookie' );
      
      Reply
  4. Ruslan

    Hi! Whe i run this command:

    wp option add vhp_varnish_ip 127.0.0.1:6081,

    i have got this error:

    Error: Could not add option 'vhp_varnish_ip'. Does it already exist?

    If i add to my config.php this: define( 'VHP_VARNISH_IP', '127.0.0.1:6081' ),
    is same thing?
    Thx in advance!

    Reply
    • Danila Vershinin

      Hi Ruslan, you need to make sure cache purge plugin is activated because it is what provides that option: wp plugin install varnish-http-purge --activate

      Reply

Leave a Reply

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.