|
######################################################################### |
|
### The perfect Varnish 4.x+ configuration ### |
|
### for WordPress, Joomla, Drupal & other (common) CMS based websites ### |
|
######################################################################### |
|
|
|
###################### |
|
# |
|
# UPDATED on December 15th, 2021 |
|
# |
|
# Configuration Notes: |
|
# 1. Default dynamic content caching respects your backend's cache-control HTTP header. |
|
# If however you need to enforce a different cache-control TTL, |
|
# do a search for "180" and replace with the new value in seconds. |
|
# Stale cache is served for up to 24 hours. |
|
# 2. Make sure you update the "backend default { ... }" section with the correct IP and port |
|
# |
|
###################### |
|
|
|
# Varnish Reference: |
|
# See the VCL chapters in the User-Guide at https://varnish-cache.org/docs/ |
|
|
|
# Marker to tell the VCL compiler that this VCL has been adapted to the new 4.1 format |
|
vcl 4.1; |
|
|
|
# Imports |
|
import std; |
|
|
|
# Default backend definition. Set this to point to your content server. |
|
backend default { |
|
.host = "127.0.0.1"; # UPDATE this only if the web server is not on the same machine |
|
.port = "8080"; # UPDATE 8080 with your web server's (internal) port |
|
} |
|
|
|
sub vcl_recv { |
|
|
|
/* |
|
# === The following are disabled by default - enable if you understand what you're doing === |
|
# Blocks |
|
if (req.http.user-agent ~ "^$" && req.http.referer ~ "^$") { |
|
return (synth(204, "No content")); |
|
} |
|
if (req.http.user-agent ~ "(ahrefs|domaincrawler|dotbot|mj12bot|semrush)") { |
|
return (synth(204, "Bot blocked")); |
|
} |
|
|
|
# List domains/subdomains to exclude from caching |
|
if (req.http.host ~ "(domain1.tld|sub.domain2.tld)") { |
|
return (pass); |
|
} |
|
*/ |
|
|
|
# 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; |
|
} |
|
} |
|
|
|
# httpoxy |
|
unset req.http.proxy; |
|
|
|
# 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); |
|
} |
|
|
|
# We only deal with GET and HEAD by default |
|
if (req.method != "GET" && req.method != "HEAD") { |
|
return (pass); |
|
} |
|
|
|
# === URL manipulation === |
|
# Remove tracking query string parameters associated with analytics/social services, useless for our backend |
|
if (req.url ~ "(\?|&)(_bta_[a-z]+|cof|cx|fbclid|gclid|ie|mc_[a-z]+|origin|siteurl|utm_[a-z]+|zanpid)=") { |
|
set req.url = regsuball(req.url, "(_bta_[a-z]+|cof|cx|fbclid|gclid|ie|mc_[a-z]+|origin|siteurl|utm_[a-z]+|zanpid)=[-_A-z0-9+()%.]+&?", ""); |
|
set req.url = regsub(req.url, "[?|&]+$", ""); |
|
} |
|
|
|
# Strip a trailing ? if it exists |
|
if (req.url ~ "\?$") { |
|
set req.url = regsub(req.url, "\?$", ""); |
|
} |
|
|
|
# Strip hash, server doesn't need it. |
|
if (req.url ~ "\#") { |
|
set req.url = regsub(req.url, "\#.*$", ""); |
|
} |
|
|
|
# === Generic cookie manipulation === |
|
# Collapse multiple cookie headers into one |
|
std.collect(req.http.Cookie); |
|
|
|
# Remove common cookies associated with analytics/social services (inc. WordPress test cookies) |
|
set req.http.Cookie = regsuball(req.http.Cookie, "(has_js|__utm.|_ga|_gat|utmctr|utmcmd.|utmccn.|__gads|__qc.|__atuv.|wp-settings-1|wp-settings-time-1|wordpress_test_cookie)=[^;]+(; )?", ""); |
|
|
|
# Remove a ";" prefix in the cookie if present |
|
set req.http.Cookie = regsuball(req.http.Cookie, "^;\s*", ""); |
|
|
|
# Remove blank cookies |
|
if (req.http.cookie ~ "^\s*$") { |
|
unset req.http.cookie; |
|
} |
|
|
|
# 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; |
|
} |
|
|
|
# === DO NOT CACHE === |
|
# Don't cache HTTP authorization/authentication pages and pages with certain headers or cookies |
|
if ( |
|
req.http.Authorization || |
|
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); |
|
} |
|
|
|
# === 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); |
|
} |
|
|
|
return (hash); |
|
|
|
} |
|
|
|
sub vcl_backend_response { |
|
|
|
/* |
|
# === The following are disabled by default - enable if you understand what you're doing === |
|
# List domains/subdomains to exclude from caching |
|
if (bereq.http.host ~ "(domain1.tld|sub.domain2.tld)") { |
|
set beresp.uncacheable = true; |
|
return (deliver); |
|
} |
|
*/ |
|
|
|
# Don't cache 50x responses |
|
if ( |
|
beresp.status == 500 || |
|
beresp.status == 502 || |
|
beresp.status == 503 || |
|
beresp.status == 504 |
|
) { |
|
return (abandon); |
|
} |
|
|
|
# === 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" || |
|
bereq.url ~ "^/wp-login.php" || |
|
bereq.url ~ "^\?add-to-cart=" || |
|
bereq.url ~ "^\?wc-api=" |
|
) { |
|
#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 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_[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 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 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 "pragma" header (suggested) |
|
unset beresp.http.Pragma; |
|
|
|
# Unset the "vary" header (suggested) |
|
unset beresp.http.Vary; |
|
|
|
# Unset the "etag" header (optional) |
|
#unset beresp.http.etag; |
|
|
|
# Allow stale content, in case the backend goes down |
|
set beresp.grace = 24h; |
|
|
|
# Enforce your own cache TTL (optional) |
|
#set beresp.ttl = 180s; |
|
|
|
# Modify "expires" header (optional) |
|
#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"; |
|
#} |
|
|
|
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 (180s 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") { |
|
set beresp.http.Cache-Control = "public, max-age=180, stale-while-revalidate=360, stale-if-error=43200"; |
|
} |
|
|
|
# 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 { |
|
|
|
/* |
|
# === The following are disabled by default - enable if you understand what you're doing === |
|
# Send a special header for excluded domains/subdomains only |
|
# The if statement can be identical to the ones in the vcl_recv() and vcl_backend_response() functions above |
|
if (req.http.host ~ "(domain1.tld|sub.domain2.tld)") { |
|
set resp.http.X-Domain-Status = "EXCLUDED"; |
|
} |
|
|
|
# Enforce redirect to HTTPS for specified domains/subdomains only |
|
if ( |
|
req.http.host ~ "(domain3.tld|sub.domain4.tld)" && |
|
req.http.X-Forwarded-Proto !~ "(?i)https" |
|
) { |
|
set resp.http.Location = "https://" + req.http.host + req.url; |
|
set resp.status = 301; |
|
} |
|
*/ |
|
|
|
# Send special headers that indicate the cache status of each web page |
|
if (obj.hits > 0) { |
|
set resp.http.X-Cache = "HIT"; |
|
set resp.http.X-Cache-Hits = obj.hits; |
|
} else { |
|
set resp.http.X-Cache = "MISS"; |
|
} |
|
|
|
return (deliver); |
|
|
|
} |
Configurations updated on December 15th, 2021. They include improved URL & cookie filtering as well as refined comments. Both configurations for Varnish 3.x and 4.x+ are now much more inline too, with the exception of newer features in 4.x+ that cannot be used in 3.x (e.g. the cookie collapsing option).
Enjoy ;)