Shopware HTTP Cache with Varnish XKey
Starting with version 6.7.0, Shopware has removed Redis as a dependency for using Varnish as an HTTP cache and provides an alternative via Varnish XKey. Varnish XKey is part of the Varnish Module Collection, which is not integrated into Varnish Cache itself. We provide a corresponding Debian repository that allows you to install the Varnish Module Collection via apt, thereby simplifying the integration of Varnish XKey.
For configuring Varnish with XKey, Shopware provides a complete configuration guide in the Shopware developer documentation:
Prerequisites
- Shopware 6 Cluster
- Varnish server running Varnish 7.7
- Varnish Modules Collection
For managed servers, our support team will handle the installation of Varnish 7.7 and the Varnish Modules Collection. If you would like to install the software yourself, please feel free to contact our support team.
Trusted Proxies
Enter the source IP address of the proxy server in the TRUSTED_PROXIES configuration. We recommend operating within a creoline VPC network to demilitarize the internal infrastructure.
Starting with Shopware 6.6, you must first configure a framework.yaml file to enable the Shopware-internal TRUSTED_PROXIES configuration.
# config/packages/framework.yaml
framework:
trusted_proxies: '%env(TRUSTED_PROXIES)%' Example configuration via .env:
TRUSTED_PROXIES=127.0.0.1,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 proxies.
For more information on trusted proxies, see the official Symfony documentation at Symfony Proxy Docs.
Shopware 6 XKey Configuration
To ensure that Shopware 6 sets the correct Cache-Control header, the reverse proxy must be configured. The following configuration ensures that Shopware sets the Cache-Control header value to public, no-cache, and that it invalidates the caches on the Varnish servers accordingly via BAN requests.
Before configuring Shopware, it’s worth taking a look at the Error Analysis and, if necessary, make the settings described there in advance. This usually helps avoid downtime, since it is difficult to estimate in advance—and, for example, without a staging cluster—how much overhead will be generated in the HTTP headers.
Create the following file and insert the content below:
nano config/packages/shopware.yml # Note that the configuration key has changed from `storefront.reverse_proxy` to `shopware.http_cache.reverse_proxy starting with Shopware 6.6
shopware:
http_cache:
reverse_proxy:
enabled: true
use_varnish_xkey: true
hosts:
# The names for App-Slave servers start at 2, since No. 1 is the App-Master server
- 'http://10.20.0.XX' # <- Varnish cache for App-Slave 2 (10.20.0.X)
- 'http://10.20.0.ZZ' # <- Varnish cache for App Slave 3 (10.20.0.Z) Additionally, the environment variable SHOPWARE_HTTP_CACHE_ENABLED=1 must be set in the .env file.
Varnish 7.7 Configuration
The Varnish configuration can be edited and deployed directly via the configuration module in the Customer Center. Navigate to Server → Server X → Configuration Files → Varnish, to customize the Varnish configuration.
Please note that saving changes to the Varnish configuration immediately triggers a configuration test followed by a reload of the Varnish instance. If you are configuring Varnish Cache for the first time, we recommend using a test instance to verify that the configuration is correct.
Shopware 6 Varnish XKey VCL
We provide the Shopware 6 Varnish XKey VCL and are happy to assist you with any questions regarding integration. Please note that ensuring the proper functioning of the Varnish cache in your environment is your responsibility. If plugins are used that do not fully replicate Shopware 6’s HTTP caching behavior, this may lead to unexpected cache behavior. We therefore recommend testing the Varnish cache first in a test instance or, ideally, in a staging cluster to identify potential conflicts early on.
The latest Shopware 6 Varnish VCL can be downloaded directly from GitHub:
vcl 4.1;
import std;
import xkey;
import cookie;
# Specify your app nodes here. Use round-robin balancing to add more than one.
backend default {
.host = "__SHOPWARE_BACKEND_HOST__";
.port = "__SHOPWARE_BACKEND_PORT__";
}
# ACL for purgers' IPs. (This must include app server IPs)
acl purgers {
"127.0.0.1";
"localhost";
"::1";
__SHOPWARE_ALLOWED_PURGER_IP__;
}
sub vcl_recv {
# Handle PURGE
if (req.method == "PURGE") {
if (client.ip !~ purgers) {
return (synth(403, "Forbidden"));
}
if (req.http.xkey) {
set req.http.n-gone = xkey.purge(req.http.xkey);
return (synth(200, "Invalidated "+req.http.n-gone+" objects"));
} else {
return (purge);
}
}
if (req.method == "BAN") {
if (client.ip !~ purgers) {
return (synth(403, "Forbidden"));
}
ban("req.url ~ " + req.url);
return (synth(200, "BAN URLs containing (" + req.url + ") done."));
}
# Only handle relevant HTTP request methods
if (req.method != "GET" &&
req.method != "HEAD" &&
req.method != "PUT" &&
req.method != "POST" &&
req.method != "PATCH" &&
req.method != "TRACE" &&
req.method != "OPTIONS" &&
req.method != "DELETE") {
return (pipe);
}
if (req.http.Authorization) {
return (pass);
}
# We only handle GET and HEAD by default
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Micro-optimization: Always pass these paths directly to PHP without caching
# to prevent hashing and cache lookup overhead
# Note: virtual URLs might bypass this rule (e.g., /en/checkout)
if (req.url ~ "^/(checkout|account|admin|api)(/.*)?$") {
return (pass);
}
cookie.parse(req.http.cookie);
# Set the cache-hash cookie value to the header for hashing based on the Vary header
# If the header is provided directly, it takes precedence
if (!req.http.sw-cache-hash) {
set req.http.sw-cache-hash = cookie.get("sw-cache-hash");
}
set req.http.currency = cookie.get("sw-currency");
set req.http.states = cookie.get("sw-states");
if (req.url == "/widgets/checkout/info" && (req.http.sw-cache-hash == "" || (cookie.isset("sw-states") && req.http.states !~ "cart-filled"))) {
return (synth(204, ""));
}
# Ignore query strings that are only necessary for the JavaScript on the client. Customize as needed.
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 RFC 3986, Section 2.3 "Unreserved Characters" for the 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);
# Set a header announcing Surrogate Capability to the origin
set req.http.Surrogate-Capability = "shopware=ESI/1.0";
# Ensure that the client's IP address is forwarded 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;
}
return (hash);
}
sub vcl_hash {
# Consider Shopware HTTP cache cookies
if (req.http.sw-cache-hash != "") {
hash_data("+context=" + req.http.sw-cache-hash);
} elseif (req.http.currency != "") {
hash_data("+currency=" + req.http.currency);
}
}
sub vcl_hit {
# Consider client states for response headers
if (req.http.states) {
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_backend_fetch {
unset bereq.http.currency;
unset bereq.http.states;
}
sub vcl_backend_response {
# Serve stale content for three days after the object expires
set beresp.grace = 3d;
unset beresp.http.X-Powered-By;
unset beresp.http.Server;
# This should happen before any early return via `deliver`, so that ESI can still be processed
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
# Reduce the "hit-for-miss" duration for dynamically uncacheable responses
if (beresp.http.sw-dynamic-cache-bypass == "1") {
# Mark as "Hit-For-Miss" for the next n seconds
set beresp.ttl = 1s;
set beresp.uncacheable = true;
unset beresp.http.sw-dynamic-cache-bypass;
return (deliver);
}
if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") {
set beresp.do_gzip = true;
}
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
unset beresp.http.Set-Cookie;
}
}
sub vcl_deliver {
## we don't want the client to cache anything except assets and store-api responses
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(theme|media|thumbnail|bundles|store-api)/") {
set resp.http.Pragma = "no-cache";
set resp.http.Expires = "-1";
set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
}
# 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";
}
# invalidation headers are for internal use only
unset resp.http.sw-invalidation-states;
unset resp.http.xkey;
unset resp.http.X-Varnish;
unset resp.http.Via;
unset resp.http.Link;
} Define the Backend
To enable easy vertical scaling in our clusters, a Varnish cache is always operated in a 1:1 ratio with the app slaves. This means that with 2 app slave servers, there must also be 2 Varnish cache servers in the setup.
In the default backend, the app slave server assigned in a 1:1 ratio is specified. Example for Varnish server #2:
backend default {
.host = "10.20.0.Y"; # <- VPC IP address of App Slave #2
.port = "80"; # <- Web server port for 10.20.0.Y
} Define purgers
Purgers are the servers authorized to clear the cache via a BAN request. These are primarily the app servers. All app servers must be specified.
# ACL for purger IPs. (This must contain app server IPs)
acl purgers {
"127.0.0.1";
"localhost";
"::1";
"10.20.0.X"; # <- VPC IP address of App Master #1
"10.20.0.Y"; # <- VPC IP address of App Slave #2
"10.20.0.Z"; # <- VPC IP address of App Slave #3
# ...
} Configure Load Balancer to Allow BAN Requests via URL
Varnish XKey also supports banning via the path shop.creoline-demo.de/products, so that only specific sections of the website are removed from the HTTP cache. Often, the BAN request is resolved directly via the domain name, which is why the load balancer must also be configured as a purger in this case.
You must ensure that the load balancer only allows BAN requests from specific IP addresses. For example, the WAN IPv4 address of the app servers can be allowed in the load balancer’s configuration. Otherwise, third parties could clear the cache, causing your website to lose performance.
# ACL for purgers' IPs. (This must contain app server IPs)
acl purgers {
"127.0.0.1";
"localhost";
"::1";
"10.20.0.X"; # <- VPC IP address of App Master #1
"10.20.0.Y"; # <- VPC IP address of App Slave #2
"10.20.0.Z"; # <- VPC IP address of App Slave #3
# ...
"10.20.0.1"; # <- VPC IP address of Load Balancer
} Soft Purge vs. Hard Purge
The Varnish configuration above uses hard purges by default, which ensures that a page is removed from the cache and that the next request takes longer. To counteract this, soft purges can be used, which first serve the outdated page to the client and then update the cache for that webpage in the background.
Soft purges can be enabled by modifying the Varnish configuration; see line 27 in the Varnish configuration above:
# Hard Purge
set req.http.n-gone = xkey.purge(req.http.xkey);
# Soft purge
set req.http.n-gone = xkey.softpurge(req.http.xkey); HTTP Cache for Logged-In Users or Visitors with Shopping Carts
By default, the Shopware HTTP cache is only available to non-logged-in visitors without shopping cart contents. If you do not use any customizations for logged-in users, the HTTP cache can also be enabled for logged-in users with shopping carts.
Before enabling this feature, make sure you are not providing prices based on customers or customer groups.
# config/packages/prod/shopware.yaml
shopware:
cache:
invalidation:
http_cache: [] Debugging
The default configuration removes all HTTP headers except for the Age header, which is used to determine the cache age. An Age of 0 means that caching is not working. This is usually because the Cache-Control: public HTTP header was not set by the web application.
To verify this, you can use the curl command as follows:
curl -vvv -H 'Host: <sales-channel-domain>' <app-server-ip> 1> /dev/null which should return the following response:
< HTTP/1.1 200 OK
< Cache-Control: public, s-maxage=7200
< Content-Type: text/html; charset=UTF-8
< Xkey: theme.sw-logo-desktop, ... If the Cache-Control: public or xkey: … HTTP header is not found in the response, this is likely due to an incorrect configuration in the web application itself.
Check the Shopware 6 configuration to ensure that reverse proxy mode has been correctly enabled in shopware.yaml.
Error Analysis
HTTP Status 503 Backend Fetch Failed on Individual Product and/or Category Pages
Depending on your store’s requirements, the size of the HTTP header may exceed the default size configured in Varnish for one or more product and category pages.
Check
Connect to your Varnish server via SSH and run the following command:
curl -I -H "Host: <sales-channel-domain>" -H "X-Forwarded-Proto: https" http://<app-server-ip>/path/with/error/503 | wc -c If the result exceeds 8192, you must adjust the Varnish cache Systemd service to set new default sizes.
Solution
Root privileges are required for this; for managed servers, the file is now available via the configuration module in the Customer Center. If the file is not available, please contact our support team.
Execute the following command as the root user:
systemctl edit varnish Adjust the ExecStart setting as follows:
[Service]
ExecStart=/usr/sbin/varnishd -a :80 -a localhost:8443,PROXY -f /etc/varnish/default.vcl -P %t/%N/varnishd.pid -p feature=+http2 -s malloc,2g \
-p http_req_hdr_len=32k \
-p http_resp_hdr_len=32k \
-p http_resp_size=64k
Restart=on-failure Next, reload the Varnish cache to apply the configuration.
systemctl reload varnish If your server is a managed server, commands that must be executed as the root user can only be carried out by our support team. Please contact our support team so that we can make the desired changes.
HTTP Status Error 502 Bad Gateway
Checking FastCGI Proxy Buffers
It is possible that the size of the FastCGI proxy buffers in Nginx is insufficient for Varnish XKey, which is why the following settings must be specified in the server or location directive.
Check the corresponding Nginx error log file for the following error message:
The filename error.log may differ depending on your configuration.
grep "upstream sent too big header" /var/log/nginx/error.log Solution
Increase the following settings within your Nginx server or location directive as follows:
The filename website.conf may differ from your configuration.
nano /etc/nginx/conf.d/website.conf Option 1: Server Directive
server {
...
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
...
} Option 2: Location Directive
location XXX {
...
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
...
} Test and then reload the Nginx configuration:
nginx -t && systemctl reload nginx