Your cart is currently empty!
Services built by a real sysadmin.
Every product in this shop was tested in production, optimized under pressure, and designed to make your infrastructure safer, faster, and cleaner.
Browse servicesComplete Guide: Setting Up Varnish Cache for WordPress with NGINX

Using Varnish Cache in front of a WordPress site can drastically improve performance, reduce server load, and serve content lightning fast — especially when paired with NGINX and Cloudflare. Below is a complete and production-ready setup, including all configuration files and best practices.
Requirements
- A Linux server (Ubuntu/Debian/CentOS)
- WordPress installed and working
- NGINX (used as SSL terminator + PHP backend)
- PHP-FPM (for dynamic content)
- Varnish installed
- Cloudflare (optional but supported)
1. Varnish Configuration (default.vcl
)
Location: usually in
/etc/varnish/default.vcl
Varnish Port: here we use:6081
for external and:6216
for internal access
vcl 4.0;
import std;
import proxy;
# Define individual backends
backend default {
.host = "127.0.0.1";
.port = "6216"; # Use 80 after SSL termination
}
# Add hostnames, IP addresses and subnets that are allowed to purge content
acl purge {
"YOUR_SERVER_IP";
"173.245.48.0"/20; # Cloudflare IPs
"103.21.244.0"/22; # Cloudflare IPs
"103.22.200.0"/22; # Cloudflare IPs
"103.31.4.0"/22; # Cloudflare IPs
"141.101.64.0"/18; # Cloudflare IPs
"108.162.192.0"/18; # Cloudflare IPs
"190.93.240.0"/20; # Cloudflare IPs
"188.114.96.0"/20; # Cloudflare IPs
"197.234.240.0"/22; # Cloudflare IPs
"198.41.128.0"/17; # Cloudflare IPs
"162.158.0.0"/15; # Cloudflare IPs
"104.16.0.0"/13; # Cloudflare IPs
"104.24.0.0"/14; # Cloudflare IPs
"172.64.0.0"/13; # Cloudflare IPs
"131.0.72.0"/22; # Cloudflare IPs
}
sub vcl_recv {
set req.backend_hint = default;
# Remove empty query string parameters
# e.g.: www.example.com/index.html?
if (req.url ~ "\?$") {
set req.url = regsub(req.url, "\?$", "");
}
# Remove port number from host header
set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
# Sorts query string parameters alphabetically for cache normalization purposes
set req.url = std.querysort(req.url);
# Remove the proxy header to mitigate the httpoxy vulnerability
# See https://httpoxy.org/
unset req.http.proxy;
# Add X-Forwarded-Proto header when using https
if (!req.http.X-Forwarded-Proto) {
if(std.port(server.ip) == 443 || std.port(server.ip) == 8443) {
set req.http.X-Forwarded-Proto = "https";
} else {
set req.http.X-Forwarded-Proto = "http";
}
}
# Purge logic to remove objects from the cache.
# Tailored to the Proxy Cache Purge WordPress plugin
# See https://wordpress.org/plugins/varnish-http-purge/
if(req.method == "PURGE") {
if(!client.ip ~ purge) {
return(synth(405,"PURGE not allowed for this IP address"));
}
if (req.http.X-Purge-Method == "regex") {
ban("obj.http.x-url ~ " + req.url + " && obj.http.x-host == " + req.http.host);
return(synth(200, "Purged"));
}
ban("obj.http.x-url == " + req.url + " && obj.http.x-host == " + req.http.host);
return(synth(200, "Purged"));
}
# 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);
}
# Remove tracking query string parameters used by analytics tools
if (req.url ~ "(\?|&)(_branch_match_id|_bta_[a-z]+|_bta_c|_bta_tid|_ga|_gl|_ke|_kx|campid|cof|customid|cx|dclid|dm_i|ef_id|epik|fbclid|gad_source|gbraid|gclid|gclsrc|gdffi|gdfms|gdftrk|hsa_acc|hsa_ad|hsa_cam|hsa_grp|hsa_kw|hsa_mt|hsa_net|hsa_src|hsa_tgt|hsa_ver|ie|igshid|irclickid|matomo_campaign|matomo_cid|matomo_content|matomo_group|matomo_keyword|matomo_medium|matomo_placement|matomo_source|mc_[a-z]+|mc_cid|mc_eid|mkcid|mkevt|mkrid|mkwid|msclkid|mtm_campaign|mtm_cid|mtm_content|mtm_group|mtm_keyword|mtm_medium|mtm_placement|mtm_source|nb_klid|ndclid|origin|pcrid|piwik_campaign|piwik_keyword|piwik_kwd|pk_campaign|pk_keyword|pk_kwd|redirect_log_mongo_id|redirect_mongo_id|rtid|s_kwcid|sb_referer_host|sccid|si|siteurl|sms_click|sms_source|sms_uph|srsltid|toolid|trk_contact|trk_module|trk_msg|trk_sid|ttclid|twclid|utm_[a-z]+|utm_campaign|utm_content|utm_creative_format|utm_id|utm_marketing_tactic|utm_medium|utm_source|utm_source_platform|utm_term|vmcid|wbraid|yclid|zanpid)=") {
set req.url = regsuball(req.url, "(_branch_match_id|_bta_[a-z]+|_bta_c|_bta_tid|_ga|_gl|_ke|_kx|campid|cof|customid|cx|dclid|dm_i|ef_id|epik|fbclid|gad_source|gbraid|gclid|gclsrc|gdffi|gdfms|gdftrk|hsa_acc|hsa_ad|hsa_cam|hsa_grp|hsa_kw|hsa_mt|hsa_net|hsa_src|hsa_tgt|hsa_ver|ie|igshid|irclickid|matomo_campaign|matomo_cid|matomo_content|matomo_group|matomo_keyword|matomo_medium|matomo_placement|matomo_source|mc_[a-z]+|mc_cid|mc_eid|mkcid|mkevt|mkrid|mkwid|msclkid|mtm_campaign|mtm_cid|mtm_content|mtm_group|mtm_keyword|mtm_medium|mtm_placement|mtm_source|nb_klid|ndclid|origin|pcrid|piwik_campaign|piwik_keyword|piwik_kwd|pk_campaign|pk_keyword|pk_kwd|redirect_log_mongo_id|redirect_mongo_id|rtid|s_kwcid|sb_referer_host|sccid|si|siteurl|sms_click|sms_source|sms_uph|srsltid|toolid|trk_contact|trk_module|trk_msg|trk_sid|ttclid|twclid|utm_[a-z]+|utm_campaign|utm_content|utm_creative_format|utm_id|utm_marketing_tactic|utm_medium|utm_source|utm_source_platform|utm_term|vmcid|wbraid|yclid|zanpid)=[-_A-z0-9+(){}%.*]+&?", "");
set req.url = regsub(req.url, "[?|&]+$", "");
}
# Only cache GET and HEAD requests
if (req.method != "GET" && req.method != "HEAD") {
set req.http.X-Cacheable = "NO:REQUEST-METHOD";
return(pass);
}
# Mark static files with the X-Static-File header, and remove any cookies
# X-Static-File is also used in vcl_backend_response to identify static files
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)(\?.*)?$") {
set req.http.X-Static-File = "true";
unset req.http.Cookie;
return(hash);
}
# No caching of special URLs, logged in users and some plugins
if (
req.http.Cookie ~ "wordpress_(?!test_)[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]+|wordpress_logged_in_|comment_author|PHPSESSID" ||
req.http.Authorization ||
req.url ~ "add_to_cart" ||
req.url ~ "edd_action" ||
req.url ~ "nocache" ||
req.url ~ "^/addons" ||
req.url ~ "^/bb-admin" ||
req.url ~ "^/bb-login.php" ||
req.url ~ "^/bb-reset-password.php" ||
req.url ~ "^/cart" ||
req.url ~ "^/checkout" ||
req.url ~ "^/control.php" ||
req.url ~ "^/login" ||
req.url ~ "^/logout" ||
req.url ~ "^/lost-password" ||
req.url ~ "^/my-account" ||
req.url ~ "^/product" ||
req.url ~ "^/register" ||
req.url ~ "^/register.php" ||
req.url ~ "^/server-status" ||
req.url ~ "^/signin" ||
req.url ~ "^/signup" ||
req.url ~ "^/stats" ||
req.url ~ "^/wc-api" ||
req.url ~ "^/wp-admin" ||
req.url ~ "^/wp-comments-post.php" ||
req.url ~ "^/wp-cron.php" ||
req.url ~ "^/wp-login.php" ||
req.url ~ "^/wp-activate.php" ||
req.url ~ "^/wp-mail.php" ||
req.url ~ "^/wp-login.php" ||
req.url ~ "^\?add-to-cart=" ||
req.url ~ "^\?wc-api=" ||
req.url ~ "^/preview=" ||
req.url ~ "^/\.well-known/acme-challenge/"
) {
set req.http.X-Cacheable = "NO:Logged in/Got Sessions";
if(req.http.X-Requested-With == "XMLHttpRequest") {
set req.http.X-Cacheable = "NO:Ajax";
}
return(pass);
}
# Cache pages with cookies for non-personalized content
if (!req.http.Cookie || req.http.Cookie ~ "wmc_ip_info|wmc_current_currency|wmc_current_currency_old") {
return(hash); # Cache the response despite cookies
}
# Remove any cookies left
unset req.http.Cookie;
return(hash);
}
sub vcl_hash {
if(req.http.X-Forwarded-Proto) {
# Create cache variations depending on the request protocol
hash_data(req.http.X-Forwarded-Proto);
}
}
sub vcl_backend_response {
# Inject URL & Host header into the object for asynchronous banning purposes
set beresp.http.x-url = bereq.url;
set beresp.http.x-host = bereq.http.host;
# If we dont get a Cache-Control header from the backend
# we default to 1h cache for all objects
if (!beresp.http.Cache-Control) {
set beresp.ttl = 1h;
set beresp.http.X-Cacheable = "YES:Forced";
}
# If the file is marked as static we cache it for 1 day
if (bereq.http.X-Static-File == "true") {
unset beresp.http.Set-Cookie;
set beresp.http.X-Cacheable = "YES:Forced";
set beresp.ttl = 1d;
}
# Remove the Set-Cookie header when a specific Wordfence cookie is set
if (beresp.http.Set-Cookie ~ "wfvt_|wordfence_verifiedHuman") {
unset beresp.http.Set-Cookie;
}
if (beresp.http.Set-Cookie) {
set beresp.http.X-Cacheable = "NO:Got Cookies";
} elseif(beresp.http.Cache-Control ~ "private") {
set beresp.http.X-Cacheable = "NO:Cache-Control=private";
}
}
sub vcl_deliver {
# Debug header
if(req.http.X-Cacheable) {
set resp.http.X-Cacheable = req.http.X-Cacheable;
} elseif(obj.uncacheable) {
if(!resp.http.X-Cacheable) {
set resp.http.X-Cacheable = "NO:UNCACHEABLE";
}
} elseif(!resp.http.X-Cacheable) {
set resp.http.X-Cacheable = "YES";
}
# Cleanup of headers
unset resp.http.x-url;
unset resp.http.x-host;
}
server {
listen 6216;
listen [::]:6216;
server_name localhost;
{{root}}
try_files $uri $uri/ /index.php?$args;
index index.php index.html;
location ~ \.php$ {
include fastcgi_params;
fastcgi_intercept_errors on;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
try_files $uri =404;
fastcgi_read_timeout 3600;
fastcgi_send_timeout 3600;
fastcgi_param HTTPS "on";
fastcgi_param SERVER_PORT 443;
fastcgi_pass 127.0.0.1:{{php_fpm_port}};
fastcgi_param PHP_VALUE "{{php_settings}}";
}
# Static files handling
location ~* ^.+\.(css|js|jpg|jpeg|gif|png|ico|gz|svg|svgz|ttf|otf|woff|woff2|eot|mp4|ogg|ogv|webm|webp|zip|swf|map)$ {
add_header Access-Control-Allow-Origin "*";
expires max;
access_log off;
}
location /.well-known/traffic-advice {
types { } default_type "application/trafficadvice+json; charset=utf-8";
}
if (-f $request_filename) {
break;
}
}
🔁 Replace
YOUR_SERVER_IP
with your actual public server IP orrealip
passed by Cloudflare.🔁 Add or replace /my-account and others, if it has been translated or changed
The vcl_recv
, vcl_backend_response
, vcl_deliver
sections include:
- Cookie & header normalization
- Full WooCommerce & WordPress compatibility
- Query string cleaning (especially for tracking)
- Support for regex-based purging via Varnish HTTP Purge plugin
👉 Tip: This config already handles X-Forwarded-Proto
, removes unneeded cookies, and bans/purges objects precisely.
2. NGINX Configuration
You’ll need:
- One server block to redirect
www
to non-www
- One public-facing block that proxies traffic to Varnish (port 6081)
- One internal block on
127.0.0.1:6216
to serve PHP files - Optional: block on port
8080
for wp-admin bypass
A. Redirect www
to non-www
server {
listen 80;
listen 443 ssl http2;
server_name www.example.com;
return 301 https://example.com$request_uri;
}
B. Public Server Block (Varnish Frontend)
server {
listen 80;
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
real_ip_header CF-Connecting-IP;
location / {
proxy_pass http://127.0.0.1:6081; # Varnish listener
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
}
location ~ \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|mp4|webp|zip)$ {
expires max;
access_log off;
}
location ~ /(wp-login\.php|wp-admin/) {
proxy_pass http://127.0.0.1:8080; # Bypass Varnish
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
C. Internal PHP Handler (for Varnish)
server {
listen 6216;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000; # Adjust to your PHP-FPM port
fastcgi_read_timeout 300;
}
}
D. Optional: wp-admin Direct Access
server {
listen 8080;
server_name example.com;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass 127.0.0.1:9000;
}
}
3. System & Service Setup
Install and Enable Varnish:
apt/yum install varnish
Edit systemd file (for port 80 → 6081):
sudo nano /etc/systemd/system/multi-user.target.wants/varnish.service
Update ExecStart
line:
ExecStart=/usr/sbin/varnishd -a :6081 -T localhost:6082 -f /etc/varnish/default.vcl -s malloc,512m
Reload systemd:
systemctl daemon-reexec
systemctl restart varnish
4. Adjust WordPress
Install plugin for auto-purging:
- Varnish HTTP Purge
- Optional: Use your own PURGE logic via
wp_remote_request()
in PHP
5. Testing the Cache
Run:
curl -I https://example.com -H "Host: example.com"
Check headers:
X-Cacheable: YES
Use varnishlog
to debug:
varnishlog -g request -q "ReqMethod eq 'GET'"
What You MUST Change:
Placeholder | Replace With |
---|---|
YOUR_SERVER_IP | Your public server IP (IPv4) |
example.com | Your real domain |
/var/www/html | Your actual WordPress root path |
127.0.0.1:9000 | Your PHP-FPM socket or port |
SSL cert paths | Use valid paths to cert.pem and key |
Varnish memory (512m ) | Adjust according to your RAM |
⚠️ Notes
- Ensure that port 6216 and 6081 are not in use by another process.
- If you’re using Cloudflare, make sure you’re not caching HTML there unless you know what you’re doing.
- Purge logic depends on the WordPress plugin, or you can implement
X-Purge-Method: regex
support manually (already included above). - Do NOT cache wp-admin or login pages.
If you need help with load balancing with Varnish or any other advanced configuration, feel free to reach out 🙂
Need Expert Help?
If you’re still having issues with your server or network setup, let’s fix it together. Schedule a one-on-one consultation now.
Schedule a Consultation
Leave a Reply