Shopware HTTP Cache with Varnish XKey
Shopware has removed Redis as a dependency for Varnish as an HTTPcache starting with version 6.7.0 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, which 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 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, you must configure the reverse proxy. The following configuration ensures that Shopware sets the public, must-revalidate value instead of the private, no-cache Cache-Control header value and invalidates the caches accordingly on the Varnish servers via BAN requests.
Before configuring Shopware, it’s worth taking a look at the Error Analysis and, if necessary, apply the settings described there beforehand. This usually helps avoid downtime, since it’s difficult to estimate in advance—and especially 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 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 with 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 the Varnish cache for the first time, we recommend using a testinstance 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 Varnish cache functions correctly in your environment is your responsibility. If you use plugins that do not fully replicate the HTTP caching behavior of Shopware 6, 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 contain 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 may 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 client-side JavaScript. 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 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);
# 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 origin.
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 {
# Take Shopware HTTP cache cookies into account
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;
}
# Reducing 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;
} Defining 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 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
# ...
} Enable Load Balancer for 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, you can specify the WAN IPv4 address of the app servers 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 the 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 adjusting 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 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 do not provide 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 the cache 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 present 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’ll need to 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.
Run 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 performed by our support team. Please contact our support team so 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