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 services

Complete Guide: Setting Up Varnish Cache for WordPress with NGINX

varnish cache vcl rules 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 or realip 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:


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:

PlaceholderReplace With
YOUR_SERVER_IPYour public server IP (IPv4)
example.comYour real domain
/var/www/htmlYour actual WordPress root path
127.0.0.1:9000Your PHP-FPM socket or port
SSL cert pathsUse 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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *