Skip to content

Varnish

Installation

To install the latest version of Varnish Cache (the high-performance HTTP accelerator), we need to add the official Varnish repository.

First we make sure we have a few dependencies:

sudo -s
apt update && apt install debian-archive-keyring curl gnupg apt-transport-https

Then install the GPG key:

curl -L https://packagecloud.io/varnishcache/varnish65/gpgkey | sudo apt-key add -

Create a file named /etc/apt/sources.list.d/varnishcache_varnish65.list that contains the repository configuration below:

deb https://packagecloud.io/varnishcache/varnish65/ubuntu/ focal main
deb-src https://packagecloud.io/varnishcache/varnish65/ubuntu/ focal main

Update apt and install Varnish:

apt update && apt install varnish

By default, Varnish runs with 256mb of RAM allocated for its cache. For my own site, I want to utilise more RAM than this. I also would like to disable gzip in Varnish so that modsecurity can deal with the responses. To do this, edit the systemd config file:

nano /lib/systemd/system/varnish.service

Find the line that says:

        -s malloc,256m

and amend to whatever you wish to allocate. For example, to allocate 2GB:

        -s malloc,2048m

Also, add an extra parameter to disable gzip so that Modsecurity can read the responses:

        -p http_gzip_support=off \

Save the file and reload systemd:

systemctl daemon-reload

Enable the service:

systemctl enable varnish

VCL Files

The vcl files are kept in /etc/varnish.

By default, Varnish uses /etc/varnish.default.vcl

To create a new one, back up the original first:

mv /etc/varnish/default.vcl /etc/varnish/default.orig

And create a new /etc/varnish/default.vcl

Further Reading

Here is the VCL file that I use for my Wordpress site:

# Marker to tell the VCL compiler that this VCL has been adapted to the
# new 4.1 format.
vcl 4.1;

# Remember, you can control the TTL for different content types by setting Cache-Control
# headers in .htaccess files (with mod_expires). Varnish defaults to 2 minutes.

# Helper functions
import std;
import directors;

# Default backend definition. Set this to point to your content server.
backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

# ACL we'll use later to allow purges
# Only allow purging from specific IPs
acl purge {
    "localhost";
    "127.0.0.1";
    "example.com";
    "anotherexample.com";
    "::1";
}

sub vcl_recv {

    # Called at the beginning of a request, after the complete request has been received and parsed.
    # Its purpose is to decide whether or not to serve the request, how to do it, and, if applicable,
    # which backend to use. It is also used to modify the request

    # The below happens before we check if we have this in cache already.
    # Typically you clean up the request here, removing cookies you don't need, rewriting the request, etc.

    # Redirect http to https
    if (client.ip != "127.0.0.1" && req.http.host ~ "example.com") {
        set req.http.x-redir = "https://example.com" + req.url;
        return(synth(850, ""));
    }
/*
    # Blocks
    if (req.http.user-agent ~ "^$" && req.http.referer ~ "^$") {
        return (synth(204, "No content"));
    }
    if (req.http.user-agent ~ "(ahrefs|bingbot|domaincrawler|dotbot|mj12bot|semrush)") {
        return (synth(204, "Bot blocked"));
    }

    # If we host multiple domains on a server, here you can list the domains you DO NOT want to cache
    # The first check matches both naked & "www" subdomains. Use the second for non generic subdomains.
    if (
        req.http.host ~ "(www\.)?(domain1.com|domain2.org|domain3.net)" ||
        req.http.host ~ "(subdomain.domain4.tld|othersubdomain.domain5.tld)"
    ) {
        return (pass);
    }
*/

    # Normalize the header if it exists, remove the port (in case you're testing this on various TCP ports)
    if (req.http.Host) {
        set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
    }

    # LetsEncrypt Certbot passthrough
    if (req.url ~ "^/\.well-known/acme-challenge/") {
        return (pass);
    }

    # Forward client's IP to the backend
    if (req.restarts == 0) {
        if (req.http.X-Real-IP) {
            set req.http.X-Forwarded-For = req.http.X-Real-IP;
        } else 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;
        }
    }

    # Normalize the query arguments (but exclude for WordPress' backend)
    if (req.url !~ "wp-admin") {
        set req.url = std.querysort(req.url);
    }

    # Non-RFC2616 or CONNECT which is weird.
    if (
        req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "TRACE" &&
        req.method != "OPTIONS" &&
        req.method != "DELETE"
    ) {
        return (pipe);
    }

    # Remove the proxy header (see httpoxy.org/)
    unset req.http.proxy;

    # Allow purging
    if (req.method == "PURGE") {
        if (!client.ip ~ purge) { # purge is the ACL defined at the begining
            # Not from an allowed IP? Then die with an error.
            return (synth(405, "This IP is not allowed to send PURGE requests."));
            }
        # If you got this stage (and didn't error out above), purge the cached result
        return (purge);
    }

    # Implementing websocket support (www.varnish-cache.org/docs/4.0/users-guide/vcl-example-websockets.html)
    #if (req.http.Upgrade ~ "(?i)websocket") {
    #    return (pipe);
    #}

    # We only deal with GET and HEAD by default
    if (req.method != "GET" && req.method != "HEAD") {
        return (pass);
    }

    # === URL manipulation ===
    # First remove the Google Analytics added parameters, useless for our backend
    if (req.url ~ "(\?|&)(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=") {
        set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "");
        set req.url = regsuball(req.url, "\?(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|siteurl)=([A-z0-9_\-\.%25]+)", "?");
        set req.url = regsub(req.url, "\?&", "?");
        set req.url = regsub(req.url, "\?$", "");
    }

    # Strip hash, server doesn't need it.
    if (req.url ~ "\#") {
    set req.url = regsub(req.url, "\#.*$", "");
    }

    # Strip a trailing ? if it exists
    #if (req.url ~ "\?$") {
    #    set req.url = regsub(req.url, "\?$", "");
    #}

    # Allow banning regexes
    if (req.method == "BAN") {
        if (!client.ip ~ purge) {
            return (synth(405, "This IP is not allowed to send BAN requests."));
        }
        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"));
    }

    ##Possibility to cache admin-ajax GET requests

    if ((req.url ~ "admin-ajax.php") && !req.http.cookie ~ "wordpress_logged_in" ) {
        return (hash);
    }

    # === Generic cookie manipulation ===
    # Remove the "has_js" cookie
    set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");

    # Remove any Google Analytics based cookies
    set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "_ga=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "_gat=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "utmctr=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "utmcmd.=[^;]+(; )?", "");
    set req.http.Cookie = regsuball(req.http.Cookie, "utmccn.=[^;]+(; )?", "");

    # Remove DoubleClick offensive cookies
    set req.http.Cookie = regsuball(req.http.Cookie, "__gads=[^;]+(; )?", "");

    # Remove the Quant Capital cookies (added by some plugin, all __qca)
    set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?", "");

    # Remove the AddThis cookies
    set req.http.Cookie = regsuball(req.http.Cookie, "__atuv.=[^;]+(; )?", "");

    # Remove the wp-settings-1 cookie
    set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?", "");

    # Remove the wp-settings-time-1 cookie
    set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");

    # Remove the wp test cookie
    set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");

    # Remove a ";" prefix in the cookie if present
    set req.http.Cookie = regsuball(req.http.Cookie, "^;\s*", "");

    # Remove the PHPSESSID in members area cookie
    set req.http.Cookie = regsuball(req.http.Cookie, "PHPSESSID=[^;]+(; )?", "");

    # Are there cookies left with only spaces or that are empty?
    if (req.http.cookie ~ "^\s*$") {
        unset req.http.cookie;
    }

    #if (req.http.Cache-Control ~ "(?i)no-cache") {
    #if (client.ip ~ purge) {
    # Ignore requests via proxy caches and badly behaved crawlers
    # like msnbot that send no-cache with every request.
    #if (! (req.http.Via || req.http.User-Agent ~ "(?i)bot" || req.http.X-Purge)) {
    #set req.hash_always_miss = true; # Doesn't seems to refresh the object in the cache
    #return(purge); # Couple this with restart in vcl_purge and X-Purge header to avoid loops
    #}
    #}
    #}

    # Check for the custom "X-Logged-In" header (used by K2 and other apps) to identify
    # if the visitor is a guest, then unset any cookie (including session cookies) provided
    # it's not a POST request.
    if(req.http.X-Logged-In == "False" && req.method != "POST") {
        unset req.http.Cookie;
    }

    # MEGA DROP. Drop ALL cookies sent to WordPress, except those originating from the URLs defined.
    # This increases HITs significantly, but be careful it can also break plugins that need cookies.
    # Note: The /members/ directory had problems with PMP login and social login plugin.
    # Adding it to the exclude list here (and including it below in the "Retain cookies" list) fixed login.
    # This works better than than other cookie removal examples found on varnish's website.
    # Note phpBB directory (forumPM) also passes cookies here.
    if (!(req.url ~ "(wp-login|wp-admin|cart|my-account|checkout|addons|wordpress-social-login|m4rk|wp-login\.php|forumPM|members|&preview=true)")) {
        unset req.http.cookie;
    }

    # === DO NOT CACHE ===
    # Don't cache HTTP authorization/authentication pages and pages with certain headers or cookies
    if (
        req.http.Authorization ||
        req.method == "POST" ||
        req.http.Authenticate ||
        req.http.X-Logged-In == "True" ||
        req.http.Cookie ~ "userID" ||
        req.http.Cookie ~ "joomla_[a-zA-Z0-9_]+" ||
        req.http.Cookie ~ "(wordpress_[a-zA-Z0-9_]+|wp-postpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+)"
    ) {
        #set req.http.Cache-Control = "private, max-age=0, no-cache, no-store";
        #set req.http.Expires = "Mon, 01 Jan 2001 00:00:00 GMT";
        #set req.http.Pragma = "no-cache";
        return (pass);
    }

    # Exclude the following paths (e.g. backend admins, user pages or ad URLs that require tracking)
    # In Joomla specifically, you are advised to create specific entry points (URLs) for users to
    # interact with the site (either common user logins or even commenting), e.g. make a menu item
    # to point to a user login page (e.g. /login), including all related functionality such as
    # password reset, email reminder and so on.
    if(
        req.url ~ "^/addons" ||
        req.url ~ "^/administrator" ||
        req.url ~ "^/cart" ||
        req.url ~ "^/checkout" ||
        req.url ~ "^/component/banners" ||
        req.url ~ "^/component/socialconnect" ||
        req.url ~ "^/component/users" ||
        req.url ~ "^/connect" ||
        req.url ~ "^/contact" ||
        req.url ~ "^/login" ||
        req.url ~ "^/logout" ||
        req.url ~ "^/lost-password" ||
        req.url ~ "^/my-account" ||
        req.url ~ "^/register" ||
        req.url ~ "^/signin" ||
        req.url ~ "^/signup" ||
        req.url ~ "^/wc-api" ||
        req.url ~ "^/wp-admin" ||
        req.url ~ "^/wp-login.php" ||
        req.url ~ "^\?add-to-cart=" ||
        req.url ~ "^\?wc-api="
    ) {
        #set req.http.Cache-Control = "private, max-age=0, no-cache, no-store";
        #set req.http.Expires = "Mon, 01 Jan 2001 00:00:00 GMT";
        #set req.http.Pragma = "no-cache";
        return (pass);
    }

    # Don't cache ajax requests
    if(req.http.X-Requested-With == "XMLHttpRequest" || req.url ~ "nocache") {
        #set req.http.Cache-Control = "private, max-age=0, no-cache, no-store";
        #set req.http.Expires = "Mon, 01 Jan 2001 00:00:00 GMT";
        #set req.http.Pragma = "no-cache";
        return (pass);
    }

    # Wordpress: don't cache these special Wordpress pages
    if (req.url ~ "/feed(/)?|preview=true|m4rk|wp-(comments-post|cron|activate|mail)\.php|register\.php") {
        return (pass);
    }

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

    # Wordpress: don't cache REST API (hand-rolled APIs used by custom themes)
    if (req.url ~ "/shared-gc/includes/rest-api/") {
        return (pass);
    }

    # Wordpress: don't cache anything with a cache-breaking v=<random> parameter (see gc.loadCachedJSON() JS function)
    if (req.url ~ "(\?|&)v=0") {
        return (pass);
    }

    # Don't cache the special pages we use to generate PDFs from the Wordpress catalog site
    if (req.url ~ "/generate-catalog/") {
        return (pass);
    }

    # Let preview posts through
    if (req.url ~ "preview=true") {
        return(pass);
    }

    # Don't cache sitemaps
    if (req.url ~ "/sitemaps") {
        return(pass);
    }

    # Don't cache REST API
    if (req.url ~ "/wp-json/") {
        return(pass);
    }

    if (req.url ~ "/index.php/") {
        return(pass);
    }

    # Large static files are delivered directly to the end-user without
    # waiting for Varnish to fully read the file first.
    # Varnish 4 fully supports Streaming, so set do_stream in vcl_backend_response()
    if (req.url ~ "^[^?]*\.(7z|avi|bz2|flac|flv|gz|mka|mkv|mov|mp3|mp4|mpeg|mpg|ogg|ogm|opus|rar|tar|tgz|tbz|txz|wav|webm|wmv|xz|zip)(\?.*)?$") {
        unset req.http.Cookie;
        return (hash);
    }

    # === STATIC FILES ===
    # Properly handle different encoding types
    if (req.http.Accept-Encoding) {
        if (req.url ~ "\.(jpg|jpeg|png|gif|gz|tgz|bz2|tbz|mp3|ogg|swf)$") {
            # No point in compressing these
            unset req.http.Accept-Encoding;
        } elseif (req.http.Accept-Encoding ~ "gzip") {
            set req.http.Accept-Encoding = "gzip";
        } elseif (req.http.Accept-Encoding ~ "deflate") {
            set req.http.Accept-Encoding = "deflate";
        } else {
            # unknown algorithm (aka crappy browser)
            unset req.http.Accept-Encoding;
        }
    }

    # Remove all cookies for static files & deliver directly
    if (req.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
        unset req.http.Cookie;
        return (hash);
    }

    # Send Surrogate-Capability headers to announce ESI support to backend
    set req.http.Surrogate-Capability = "key=ESI/1.0";

    if (req.http.Authorization) {
        # Not cacheable by default
        return (pass);
    }

    # Respect the browser's desire for a fresh copy on hard refresh
    # This ban will only work if there are no further URL changes (e.g. set req.url = ...) after it.
    if (req.http.Cache-Control == "no-cache") {
        ban("req.http.host == " + req.http.host + " && req.url == " + req.url);
    }

    return (hash);
}
/*
sub vcl_pipe {
    # Called upon entering pipe mode.
    # In this mode, the request is passed on to the backend, and any further data from both the client
    # and backend is passed on unaltered until either end closes the connection. Basically, Varnish will
    # degrade into a simple TCP proxy, shuffling bytes back and forth. For a connection in pipe mode,
    # no other VCL subroutine will ever get called after vcl_pipe.

    # Note that only the first request to the backend will have
    # X-Forwarded-For set. If you use X-Forwarded-For and want to
    # have it set for all requests, make sure to have:
    # set bereq.http.connection = "close";
    # here. It is not set by default as it might break some broken web
    # applications, like IIS with NTLM authentication.

    # set bereq.http.Connection = "Close";
    # Implementing websocket support (www.varnish-cache.org/docs/4.0/users-guide/vcl-example-websockets.html)
    if (req.http.upgrade) {
    set bereq.http.upgrade = req.http.upgrade;
    }

    return (pipe);
}

sub vcl_pass {
    # Called upon entering pass mode. In this mode, the request is passed on to the backend, and the
    # backend's response is passed on to the client, but is not entered into the cache. Subsequent
    # requests submitted over the same client connection are handled normally.

    # return (pass);
}
*/
/*
sub vcl_hash {
    # Called after vcl_recv to create a hash value for the request. This is used as a key
    # to look up the object in Varnish.

    hash_data(req.url);

    if (req.http.host) {
    hash_data(req.http.host);
    } else {
    hash_data(server.ip);
    }

    # hash cookies for requests that have them
    if (req.http.Cookie) {
    hash_data(req.http.Cookie);
    }

    # Cache the HTTP vs HTTPs separately
    if (req.http.X-Forwarded-Proto) {
    hash_data(req.http.X-Forwarded-Proto);
    }

    # If the client supports compression, keep that in a different cache
    if (req.http.Accept-Encoding) {
    hash_data(req.http.Accept-Encoding);
    }

    return (lookup);
}
*/
sub vcl_hit {
    # Called when a cache lookup is successful.

    if (obj.ttl >= 0s) {
    # A pure unadultered hit, deliver it
    return (deliver);
    }

    # www.varnish-cache.org/docs/trunk/users-guide/vcl-grace.html
    # When several clients are requesting the same page Varnish will send one request to the backend and place the others
    # on hold while fetching one copy from the backend. In some products this is called request coalescing and Varnish does
    # this automatically.
    # If you are serving thousands of hits per second the queue of waiting requests can get huge. There are two potential
    # problems - one is a thundering herd problem - suddenly releasing a thousand threads to serve content might send the
    # load sky high. Secondly - nobody likes to wait. To deal with this we can instruct Varnish to keep the objects in cache
    # beyond their TTL and to serve the waiting requests somewhat stale content.

    #if (!std.healthy(req.backend_hint) && (obj.ttl + obj.grace > 0s)) {
    #return (deliver);
    #} else {
    #return (miss);
    #}

    # We have no fresh fish. Lets look at the stale ones.
    if (std.healthy(req.backend_hint)) {
        # Backend is healthy. Limit age to 10s.
        if (obj.ttl + 10s > 0s) {
            #set req.http.grace = "normal(limited)";
            return (deliver);
        }
    } else {
        # backend is sick - use full grace
        if (obj.ttl + obj.grace > 0s) {
            #set req.http.grace = "full";
            return (deliver);
        }
    }
}

sub vcl_miss {
    # Called after a cache lookup if the requested document was not found in the cache. Its purpose
    # is to decide whether or not to attempt to retrieve the document from the backend, and which
    # backend to use.

    return (fetch);
}

sub vcl_backend_response {
    # Happens after we have read the response headers from the backend.
    #
    # Here you clean the response headers, removing silly Set-Cookie headers
    # and other mistakes your backend does.


    # If we host multiple domains on a server, here you can list the domains you DO NOT want to cache
    # The first check matches both naked & "www" subdomains. Use the second for non generic subdomains.
    #if (
    #    bereq.http.host ~ "(www\.)?(domain1.com|domain2.org|domain3.net)" ||
    #    bereq.http.host ~ "(subdomain.domain4.tld|othersubdomain.domain5.tld)"
    #) {
    #    set beresp.uncacheable = true;
    #    return (deliver);
    #}


    # Pause ESI request and remove Surrogate-Control header
    if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
        unset beresp.http.Surrogate-Control;
        set beresp.do_esi = true;
    }

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

    # Don't cache 50x responses
    if (
        beresp.status == 500 ||
        beresp.status == 502 ||
        beresp.status == 503 ||
        beresp.status == 504
    ) {
        return (abandon);
    }

    if (beresp.status == 301 || beresp.status == 302) {
        # Sometimes, a 301 or 302 redirect formed via Apache's mod_rewrite can mess with the HTTP port that is being passed along.
        # This often happens with simple rewrite rules in a scenario where Varnish runs on :80 and Apache on :8080 on the same box.
        # A redirect can then often redirect the end-user to a URL on :8080, where it should be :80.
        # This may need finetuning on your setup.
        #
        # To prevent accidental replace, we only filter the 301/302 redirects for now.
        set beresp.http.Location = regsub(beresp.http.Location, ":[0-9]+", "");

        # Don't cache redirects. Otherwise they're much more difficult to debug!
        set beresp.ttl = 0s;
    }

    # === DO NOT CACHE ===
    # Exclude the following paths (e.g. backend admins, user pages or ad URLs that require tracking)
    # In Joomla specifically, you are advised to create specific entry points (URLs) for users to
    # interact with the site (either common user logins or even commenting), e.g. make a menu item
    # to point to a user login page (e.g. /login), including all related functionality such as
    # password reset, email reminder and so on.
    if(
        bereq.url ~ "^/addons" ||
        bereq.url ~ "^/administrator" ||
        bereq.url ~ "^/cart" ||
        bereq.url ~ "^/checkout" ||
        bereq.url ~ "^/component/banners" ||
        bereq.url ~ "^/component/socialconnect" ||
        bereq.url ~ "^/component/users" ||
        bereq.url ~ "^/connect" ||
        bereq.url ~ "^/contact" ||
        bereq.url ~ "^/login" ||
        bereq.url ~ "^/logout" ||
        bereq.url ~ "^/lost-password" ||
        bereq.url ~ "^/my-account" ||
        bereq.url ~ "^/register" ||
        bereq.url ~ "^/signin" ||
        bereq.url ~ "^/signup" ||
        bereq.url ~ "^/wc-api" ||
        bereq.url ~ "^/wp-admin/(?!admin-ajax\.php)" ||
        bereq.url ~ "^/wp-login.php" ||
        bereq.url ~ "^\?add-to-cart=" ||
        bereq.url ~ "^\?wc-api=" ||
        bereq.url ~ "/feed(/)?|wp-(comments-post|cron|activate|mail)\.php|register\.php" ||
        bereq.url ~ "preview=true" ||
        bereq.url ~ "/generate-catalog/" ||
        bereq.url ~ "/\?s=" ||
        bereq.url ~ "/shared-gc/includes/rest-api/" ||
        bereq.url ~ "/m4rk" ||
        bereq.url ~ "(\?|&)v=0"
    ) {
        set beresp.uncacheable = true;
        return (deliver);
    }

    # Don't cache HTTP authorization/authentication pages and pages with certain headers or cookies
    if (
        bereq.http.Authorization ||
        bereq.http.Authenticate ||
        bereq.http.X-Logged-In == "True" ||
        bereq.http.Cookie ~ "userID" ||
        bereq.http.Cookie ~ "joomla_[a-zA-Z0-9_]+" ||
        bereq.http.Cookie ~ "(wordpress_(?!test_)[a-zA-Z0-9_]+|wp-postpass|resetpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+)"
    ) {
        set beresp.uncacheable = true;
        return (deliver);
    }

    # Don't cache ajax requests
    if(beresp.http.X-Requested-With == "XMLHttpRequest" || bereq.url ~ "nocache") {
        #set beresp.http.Cache-Control = "private, max-age=0, no-cache, no-store";
        #set beresp.http.Expires = "Mon, 01 Jan 2001 00:00:00 GMT";
        #set beresp.http.Pragma = "no-cache";
        set beresp.uncacheable = true;
        return (deliver);
    }

    # Don't cache backend response to posted requests
    if (bereq.method == "POST") {
        set beresp.uncacheable = true;
        return (deliver);
    }

    # Ok, we're cool & ready to cache things
    # so let's clean up some headers and cookies
    # to maximize caching.

    # Check for the custom "X-Logged-In" header to identify if the visitor is a guest,
    # then unset any cookie (including session cookies) provided it's not a POST request.
    if(beresp.http.X-Logged-In == "False" && bereq.method != "POST") {
        unset beresp.http.Set-Cookie;
    }

    # Unset the "etag" header (suggested)
    #unset beresp.http.etag;

    # Unset the "pragma" header
    unset beresp.http.Pragma;

    # Unset the "vary" header (suggested)
    unset beresp.http.Vary;

    # Allow stale content, in case the backend goes down
    set beresp.grace = 24h;

    # This is how long Varnish will keep cached content
    #set beresp.ttl = 60m;

    # Modify "expires" header - www.varnish-cache.org/trac/wiki/VCLExampleSetExpires
    #set beresp.http.Expires = "" + (now + beresp.ttl);

    # If your backend server does not set the right caching headers for static assets,
    # you can set them below (uncomment first and change 604800 - which 1 week - to whatever you
    # want (in seconds)
    #if (bereq.url ~ "\.(ico|jpg|jpeg|gif|png|bmp|webp|tiff|svg|svgz|pdf|mp3|flac|ogg|mid|midi|wav|mp4|webm|mkv|ogv|wmv|eot|otf|woff|ttf|rss|atom|zip|7z|tgz|gz|rar|bz2|tar|exe|doc|docx|xls|xlsx|ppt|pptx|rtf|odt|ods|odp)(\?[a-zA-Z0-9=]+)$") {
    # set beresp.http.Cache-Control = "public, max-age=604800";
    #}

    # Enable cache for all static files
    # The same argument as the static caches from above: monitor your cache size, if you get data nuked out of it, consider giving up the static file cache.
    # Before you blindly enable this, have a read here: ma.ttias.be/stop-caching-static-files/
    if (bereq.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
        unset beresp.http.set-cookie;
        set beresp.do_stream = true;
    }

    # We have content to cache, but it's got no-cache or other Cache-Control values sent
    # So let's reset it to our main caching time (60m as used in this example configuration)
    # The additional parameters specified (stale-while-revalidate & stale-if-error) are used
    # by modern browsers to better control caching. Set these to twice & four times your main
    # cache time respectively.
    # This final setting will normalize cache-control headers for CMSs like Joomla
    # which set max-age=0 even when the CMS' cache is enabled.
    if (beresp.http.Cache-Control !~ "max-age" || beresp.http.Cache-Control ~ "max-age=0" || beresp.ttl < 60m) {
        set beresp.http.Cache-Control = "public, max-age=3600, stale-while-revalidate=7200, stale-if-error=14400";
    }

    # Optionally set a larger TTL for pages with less than 180s of cache TTL
    #if (beresp.ttl < 180s) {
    #    set beresp.http.Cache-Control = "public, max-age=180, stale-while-revalidate=360, stale-if-error=43200";
    #}

    return(deliver);
}

sub vcl_deliver {
    # Happens when we have all the pieces we need, and are about to send the
    # response to the client.
    #
    # You can do accounting or modifying the final object here.


    /*
    # Send a special header for excluded domains only
    # The if statement can be identical to the ones in the vcl_recv() and vcl_fetch() functions above
    if (
    req.http.host ~ "(www\.)?(domain1.com|domain2.org|domain3.net)" ||
    req.http.host ~ "(subdomain.domain4.tld|othersubdomain.domain5.tld)"
    ) {
    set resp.http.X-Domain-Status = "EXCLUDED";
    }
    # Enforce redirect to HTTPS for specified domains only
    if (
    req.http.host ~ "(subdomain.domain4.tld|othersubdomain.domain5.tld)" &&
    req.http.X-Forwarded-Proto !~ "(?i)https"
    ) {
    set resp.http.Location = "https://" + req.http.host + req.url;
    set resp.status = 302;
    }
    */
    # Send special headers that indicate the cache status of each web page
    # Please note that obj.hits behaviour changed in 4.0, now it counts per objecthead, not per object
    # and obj.hits may not be reset in some cases where bans are in use. See bug 1492 for details.
    # So take hits with a grain of salt
    if (obj.hits > 0) {
        set resp.http.X-Cache = "V-HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "V-MISS";
    }

    # Remove some headers to improve security
    unset resp.http.Server;
    unset resp.http.X-Drupal-Cache;
    unset resp.http.X-Varnish;
    unset resp.http.Age;
    unset resp.http.Via;
    unset resp.http.Link;
    unset resp.http.X-Generator;

    #if (resp.http.magicmarker) {
    # unset resp.http.magicmarker;
    # set resp.http.age = "0";
    #}
}

sub vcl_purge {
    # Only handle actual PURGE HTTP methods, everything else is discarded
    if (req.method != "PURGE") {
        # restart request
        set req.http.X-Purge = "Yes";
        return(restart);
    }
}

sub vcl_synth {
    if (resp.status == 720) {
        # We use this special error status 720 to force redirects with 301 (permanent) redirects
        # To use this, call the following from anywhere in vcl_recv: return (synth(720, "http://host/new.html"));
        set resp.http.Location = resp.reason;
        set resp.status = 301;
        return (deliver);
    } elseif (resp.status == 721) {
        # And we use error status 721 to force redirects with a 302 (temporary) redirect
        # To use this, call the following from anywhere in vcl_recv: return (synth(720, "http://host/new.html"));
        set resp.http.Location = resp.reason;
        set resp.status = 302;
        return (deliver);
    }

    return (deliver);
}

sub vcl_fini {
    # Called when VCL is discarded only after all requests have exited the VCL.
    # Typically used to clean up VMODs.

    return (ok);
}