Your cart is currently empty!
Category: Cache
Complete 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 accessvcl 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-wwwserver { 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
andkey
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 🙂