Shopware HTTP Cache with Varnish

Shopware provides a complete configuration for the configuration of Varnish in the Shopware Developer Docs:

https://developer.shopware.com/docs/guides/hosting/infrastructure/reverse-http-cache.html#configure-shopware



Trusted Proxies

The use of Symfony and Varnish is not a problem in most cases. However, when a request goes through a proxy, certain request data is sent with either the default Forwarded or X-Forwarded header. For example, instead of reading the REMOTE_ADDR header (which is now the IP address of your reverse proxy), the actual IP address of the user is stored.

If you do not configure Symfony to look for these headers, you will receive incorrect information about the client's IP address. Regardless of whether the client connects via https or not, the client's port and hostname will be queried.


Example configuration via .env:

TRUSTED_PROXIES=127.0.0.1,127.0.0.2,10.20.1.1/32,10.20.0.1/32,10.20.0.2/32


In this example configuration, the two Varnish instances 10.20.0.1 and 10.20.0.2 are authorized as a proxy.

You can find more information on this under:

Symfony Proxy Docs

creoline VPC networks



Reverse proxy configuration

In order for Shopware 6 to set the correct Cache-Control header, the configuration of the reverse proxy is required. The following configuration can be used to ensure that Shopware sets the value public, must-revalidate instead of the Cache-Control header value private, no-cache and invalidates the caches accordingly in Varnish via BAN requests.


To do this, create the following file:

config/packages/storefront.yaml


Up to Shopware 6.5.X.X

storefront:
    reverse_proxy:
        enabled: true
        ban_method: "BAN"
        # Varnish hosts (IP:Port) [Default Port: 80]
        hosts: [ "http://10.20.X.X", "http://10.20.X.X" ]

        # Additional Headers for BAN Requests (E.G. Overwrite Host Header)
        ban_headers:
            Host: "www.creoline-demo.com"

        # Max parallel invalidations at same time for a single worker
        max_parallel_invalidations: 5

        # Redis (Default DB: 0, Use /X for another database)
        redis_url: "redis://10.20.X.X:6379/0"


From Shopware 6.6.0.0

shopware:
    http_cache:
        reverse_proxy:
            enabled: true
            ban_method: "BAN"
            # Varnish hosts (IP:Port) [Default Port: 80]
            hosts: [ "http://10.20.X.X", "http://10.20.X.X" ]

            # Additional Headers for BAN Requests (E.G. Overwrite Host Header)
            ban_headers:
                Host: "www.creoline-demo.com"

            # Max parallel invalidations at same time for a single worker
            max_parallel_invalidations: 5

            # Redis (Default DB: 0, Use /X for another database)
            redis_url: "redis://10.20.X.X:6379/0"


Attention: Shopware has been using a different YAML schema since version 6.6.0.0. If you are already using Shopware with Varnish in an earlier version, the new schema should be stored accordingly before the upgrade.


Please ensure that you enter the correct IP addresses of the Varnish instances, including the HTTP ports, if this differs from the standard port 80. In the standard configuration of the creoline Varnish server, the HTTP port 80 is configured here. If you have installed Varnish independently, the default port is 6081.


Please note that the HTTP Host header in the BAN request corresponds to the IP addresses or host names of the specified hosts by default. By specifying ban_headers, the host header can be overwritten accordingly so that the correct cache key can be determined in Varnish.



Varnish configuration

The Varnish configuration can be edited and rolled out directly via the configuration module in the Customer Center. Navigate to Server → Your server → Configuration files → Varnish Config, to customize the Varnish configuration.


Please note that saving the changes to the Varnish configuration immediately triggers a config test with a subsequent reload of the Varnish instance. If you configure Varnish inital, we recommend a test instance to evaluate the correct configuration.


Example configuration with Soft-Purge:

vcl 4.0;

import std;
import purge;

# You should specify here all your app nodes and use round robin to select a backend
backend default {
    .host = "<app-host>";
    .port = "80";
}

# ACL for purgers IP. (This needs to contain app server ips)
acl purgers {
    "127.0.0.1";
    "localhost";
    "::1";
}

sub vcl_recv {
    # Mitigate httpoxy application vulnerability, see: https://httpoxy.org/
    unset req.http.Proxy;

    # Strip query strings only needed by browser javascript. Customize to used tags.
    if (req.url ~ "(\?|&)(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=") {
        # see rfc3986#section-2.3 "Unreserved Characters" for regex
        set req.url = regsuball(req.url, "(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=[A-Za-z0-9\-\_\.\~]+&?", "");
    }
    set req.url = regsub(req.url, "(\?|\?&|&&)$", "");

    # Normalize query arguments
    set req.url = std.querysort(req.url);

    # Make sure that the client ip is forward to the client.
    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;
    }

    # Handle BAN
    if (req.method == "BAN") {
        if (!client.ip ~ purgers) {
            return (synth(405, "Method not allowed"));
        }

        return (hash);
    }

    # Normalize Accept-Encoding header
    # straight from the manual: https://www.varnish-cache.org/docs/3.0/tutorial/vary.html
    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;
        }
    }

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

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

    # Don't cache Authenticate & Authorization
    if (req.http.Authenticate || req.http.Authorization) {
        return (pass);
    }

    # Always pass these paths directly to php without caching
    # Note: virtual URLs might bypass this rule (e.g. /en/checkout)
    if (req.url ~ "^/(checkout|account|admin|api)(/.*)?$") {
        return (pass);
    }

    return (hash);
}

sub vcl_hash {
    # Consider Shopware HTTP cache cookies
    if (req.http.cookie ~ "sw-cache-hash=") {
        hash_data("+context=" + regsub(req.http.cookie, "^.*?sw-cache-hash=([^;]*);*.*$", "\1"));
    } elseif (req.http.cookie ~ "sw-currency=") {
        hash_data("+currency=" + regsub(req.http.cookie, "^.*?sw-currency=([^;]*);*.*$", "\1"));
    }
}

sub vcl_hit {
    if (req.method == "BAN") {
        call soft_purge_page;
    }

    # Consider client states for response headers
    if (req.http.cookie ~ "sw-states=") {
        set req.http.states = regsub(req.http.cookie, "^.*?sw-states=([^;]*);*.*$", "\1");

        if (req.http.states ~ "logged-in" && obj.http.sw-invalidation-states ~ "logged-in" ) {
            return (pass);
        }

        if (req.http.states ~ "cart-filled" && obj.http.sw-invalidation-states ~ "cart-filled" ) {
            return (pass);
        }
    }
}

sub vcl_miss {
    if (req.method == "BAN") {
        call soft_purge_page;
    }
}

sub vcl_backend_response {
    # Fix Vary Header in some cases
    # https://www.varnish-cache.org/trac/wiki/VCLExampleFixupVary
    if (beresp.http.Vary ~ "User-Agent") {
        set beresp.http.Vary = regsub(beresp.http.Vary, ",? *User-Agent *", "");
        set beresp.http.Vary = regsub(beresp.http.Vary, "^, *", "");
        if (beresp.http.Vary == "") {
            unset beresp.http.Vary;
        }
    }

    # Respect the Cache-Control=private header from the backend
    if (
        beresp.http.Pragma ~ "no-cache" ||
        beresp.http.Cache-Control ~ "no-cache" ||
        beresp.http.Cache-Control ~ "private"
    ) {
        set beresp.ttl = 0s;
        set beresp.http.X-Cacheable = "NO:Cache-Control=private";
        set beresp.uncacheable = true;
        return (deliver);
    }

    # strip the cookie before the image is inserted into cache.
    if (bereq.url ~ "\.(png|gif|jpg|swf|css|js|webp)$") {
        unset beresp.http.set-cookie;
    }

    # Allow items to be stale if needed.
    set beresp.grace = 6h;

    # Save the bereq.url so bans work efficiently
    set beresp.http.x-url = bereq.url;
    set beresp.http.X-Cacheable = "YES";

    # Remove the exact PHP version from the response for more security
    unset beresp.http.x-powered-by;

    return (deliver);
}

sub vcl_deliver {
    ## we don't want the client to cache
    set resp.http.Cache-Control = "max-age=0, private";

    # remove link header, if session is already started to save client resources
    if (req.http.cookie ~ "session-") {
        unset resp.http.Link;
    }

    # Set a cache header to allow us to inspect the response headers during testing
    if (obj.hits > 0) {
        unset resp.http.set-cookie;
        set resp.http.X-Cache = "HIT";

        if (obj.ttl <= 0s && obj.grace > 0s) {
            set resp.http.X-Cache = "STALE";
        }
    } else {
        set resp.http.X-Cache = "MISS";
    }

    # Remove the exact PHP version from the response for more security (e.g. 404 pages)
    unset resp.http.x-powered-by;

    # Remove additional information about Varnish
    unset resp.http.via;
    unset resp.http.x-varnish;

    # invalidation headers are only for internal use
    unset resp.http.sw-invalidation-states;

    set resp.http.X-Cache-Hits = obj.hits;
    set resp.http.X-Cache-Lifetime = obj.ttl;
}

sub soft_purge_page {
    # See https://docs.varnish-software.com/varnish-cache-plus/vmods/purge/ for all possible options
    set req.http.purged = purge.soft(ttl = 0s, grace = 300s, keep = 3600s);
    return (synth(200));
}



HTTP cache for logged-in users or visitors with shopping carts

In the default setting, the Shopware HTTP cache is only available for users who are not logged in and visitors without shopping cart content. If you do not use customizations for logged-in users, the HTTP cache can also be activated for logged-in users or visitors with shopping carts.


# config/packages/prod/shopware.yaml
shopware:
    cache:
        invalidation:
            http_cache: []