Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Varnish 4 VCL configuration for WordPress. Also allows purging
# A heavily customized VCL to support WordPress
# Some items of note:
# Supports https
# Supports admin cookies for wp-admin
# Caches everything
# Support for custom error html page
vcl 4.0;
import directors;
import std;
# Assumed 'wordpress' host, this can be docker servicename
backend default {
.host = "wordpress";
.port = "80";
}
acl purge {
"localhost";
"127.0.0.1";
}
sub vcl_recv {
# Only a single backend
set req.backend_hint= default;
# Setting http headers for backend
set req.http.X-Forwarded-For = client.ip;
set req.http.X-Forwarded-Proto = "https";
# Unset headers that might cause us to cache duplicate infos
unset req.http.Accept-Language;
unset req.http.User-Agent;
# The purge...no idea if this works
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return(synth(405,"Not allowed."));
}
ban("req.url ~ /");
return (purge);
}
if ( std.port(server.ip) == 6080) {
set req.http.x-redir = "https://" + req.http.host + req.url;
return (synth(750, "Moved permanently"));
}
# drop cookies and params from static assets
if (req.url ~ "\.(gif|jpg|jpeg|swf|ttf|css|js|flv|mp3|mp4|pdf|ico|png)(\?.*|)$") {
unset req.http.cookie;
set req.url = regsub(req.url, "\?.*$", "");
}
# drop tracking params
if (req.url ~ "\?(utm_(campaign|medium|source|term)|adParams|client|cx|eid|fbid|feed|ref(id|src)?|v(er|iew))=") {
set req.url = regsub(req.url, "\?.*$", "");
}
# pass wp-admin urls
if (req.url ~ "(wp-login|wp-admin)" || req.url ~ "preview=true" || req.url ~ "xmlrpc.php") {
return (pass);
}
# pass wp-admin cookies
if (req.http.cookie) {
if (req.http.cookie ~ "(wordpress_|wp-settings-)") {
return(pass);
} else {
unset req.http.cookie;
}
}
}
sub vcl_backend_response {
# retry a few times if backend is down
if (beresp.status == 503 && bereq.retries < 3 ) {
return(retry);
}
if (bereq.http.Cookie ~ "(UserID|_session)") {
# if we get a session cookie...caching is a no-go
set beresp.http.X-Cacheable = "NO:Got Session";
set beresp.uncacheable = true;
return (deliver);
} elsif (beresp.ttl <= 0s) {
# Varnish determined the object was not cacheable
set beresp.http.X-Cacheable = "NO:Not Cacheable";
} elsif (beresp.http.set-cookie) {
# You don't wish to cache content for logged in users
set beresp.http.X-Cacheable = "NO:Set-Cookie";
set beresp.uncacheable = true;
return (deliver);
} elsif (beresp.http.Cache-Control ~ "private") {
# You are respecting the Cache-Control=private header from the backend
set beresp.http.X-Cacheable = "NO:Cache-Control=private";
set beresp.uncacheable = true;
return (deliver);
} else {
# Varnish determined the object was cacheable
set beresp.http.X-Cacheable = "YES";
# Remove Expires from backend, it's not long enough
unset beresp.http.expires;
# Set the clients TTL on this object
set beresp.http.cache-control = "max-age=900";
# Set how long Varnish will keep it
set beresp.ttl = 1w;
# marker for vcl_deliver to reset Age:
set beresp.http.magicmarker = "1";
}
# unset cookies from backendresponse
if (!(bereq.url ~ "(wp-login|wp-admin)")) {
set beresp.http.X-UnsetCookies = "TRUE";
unset beresp.http.set-cookie;
set beresp.ttl = 1h;
}
# long ttl for assets
if (bereq.url ~ "\.(gif|jpg|jpeg|swf|ttf|css|js|flv|mp3|mp4|pdf|ico|png)(\?.*|)$") {
set beresp.ttl = 365d;
}
set beresp.grace = 1w;
}
sub vcl_hash {
if ( req.http.X-Forwarded-Proto ) {
hash_data( req.http.X-Forwarded-Proto );
}
}
sub vcl_backend_error {
# display custom error page if backend down
if (beresp.status == 503 && bereq.retries == 3) {
synthetic(std.fileread("/etc/varnish/error503.html"));
return(deliver);
}
}
sub vcl_synth {
# redirect for http
if (resp.status == 750) {
set resp.status = 301;
set resp.http.Location = req.http.x-redir;
return(deliver);
}
# display custom error page if backend down
if (resp.status == 503) {
synthetic(std.fileread("/etc/varnish/error503.html"));
return(deliver);
}
}
sub vcl_deliver {
# oh noes backend is down
if (resp.status == 503) {
return(restart);
}
if (resp.http.magicmarker) {
# Remove the magic marker
unset resp.http.magicmarker;
# By definition we have a fresh object
set resp.http.age = "0";
}
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
set resp.http.Access-Control-Allow-Origin = "*";
}
sub vcl_hit {
if (req.method == "PURGE") {
return(synth(200,"OK"));
}
}
sub vcl_miss {
if (req.method == "PURGE") {
return(synth(404,"Not cached"));
}
}
@makmour

This comment has been minimized.

Copy link

commented Feb 28, 2016

Hi!
There is something worng with

 if ( (!(bereq.url ~ "(wp-(login|admin)|login)")) || (bereq.method == "GET") ) {
        unset beresp.http.set-cookie;
        set beresp.ttl = 1s;
        }

because it makes WP to fail loading the test cookie thus not permitting users to login into the Dashboard,

@matthewjackowski

This comment has been minimized.

Copy link
Owner Author

commented Mar 14, 2016

@makmour I reduced the logic on that condition and also set an X-param to be able to see when it triggers. Seems to be ok for me on my local docker setup. Keep in mind that to enable https you have todo alot of tomfoolery in Wordpress too.

@oralunal

This comment has been minimized.

Copy link

commented Dec 29, 2016

It works great, thank you sir

@marco41

This comment has been minimized.

Copy link

commented Jul 14, 2017

Hi, it works great with https. But not all my sites are https. I can not log in to my sites with http. How do I add a code for this to help you?

@danhere

This comment has been minimized.

Copy link

commented Sep 4, 2017

Thanks for this! There's a surprising lack of good/up to date configs out there.

I did run into small issue though: Have you noticed that Wordpress' Visual Editor doesn't load with this config? I disabled Varnish all together and the Visual Editor comes right back. I can't seem to figure it out. Any ideas?

@marco41

This comment has been minimized.

Copy link

commented Sep 24, 2017

Hello,
Normally : http://prntscr.com/gp860w
But it looks like this. : http://prntscr.com/gp85ws

How can my overcome this problem?

@dockrize

This comment has been minimized.

Copy link

commented Sep 25, 2017

@marco41 @danhere
Check the HTTP requests at your browser (developer console). I saw this problem when doing HTTP requests to a HTTPS port.

@marco41

This comment has been minimized.

Copy link

commented Sep 28, 2017

@dockrize what should I do. Please tell me in detail.

@marco41

This comment has been minimized.

Copy link

commented Oct 8, 2017

@matthewjackowski If you know, please answer me?

@informdev

This comment has been minimized.

Copy link

commented Dec 13, 2017

So far this is excellent. I keep reading about issues people have ran into with CloudFlare and Varnish -- I'm still new to this stuff but so far it SEEMS to be working? Any thoughts, ideas? fixes if an issue?

@informdev

This comment has been minimized.

Copy link

commented Dec 13, 2017

To everyone else @Marco31 @danhere -- you can fix your editor issues by bypassing caching if logged in.
Under sub vcl_recv { add the following:
if (req.url ~ "wp-(login|admin|comments-post.php|cron.php)" ||
req.url ~ "preview=true" ||
req.url ~ "xmlrpc.php") {
return (pass);
}

@marco41

This comment has been minimized.

Copy link

commented Jan 8, 2018

@informdev thank you so much. My problem is resolved.

@hazhayder

This comment has been minimized.

Copy link

commented Mar 23, 2018

It shows apache default page when i ran this vcl file.

@xoroz

This comment has been minimized.

Copy link

commented Apr 12, 2018

Tried all configurations I still have the same problem as Marco41...
TinyMCE does not work once varnish is activated. Without it, it works!

Please someone help us out.

I noticed without varnish these URLs

http://felipeferreira.net/wp-content/themes/bandana/css/editor-style.css?wp-mce-4607-20180123-tadv-4.6.7
http://felipeferreira.net/wp-includes/js/tinymce/plugins/compat3x/plugin.min.js?ver=4607-20180123-tadv-4.6.7
http://felipeferreira.net/wp-includes/js/tinymce/tinymce.min.js?ver=4607-20180123-tadv-4.6.7
http://felipeferreira.net/wp-includes/js/tinymce/skins/wordpress/wp-content.css?ver=4.9.5&wp-mce-4607-20180123-tadv-4.6.7

Then with varnish ON, I no longer see those, and I see something trunked like:
http://felipeferreira.net/wp-includes/js/mce-view.min.js?ver=4.9.5

have been struginling with this problem for over 2 years now, I have tried
if (req.url ~ "wp-(login|admin|comments-post.php|cron.php)" ||
req.url ~ "preview=true" ||
req.url ~ "xmlrpc.php") {
return (pass);
}
if (req.url ~ "(tinymce|wp-mce|plugin.min.js)" ||
req.url ~ "preview=true" ||
req.url ~ "xmlrpc.php") {
return (pass);
}

            if (req.http.Cookie ~ "(wordpress_|comment_|wp-settings-)") {
                    return (pass);
            }

but did not work...

@arpan-jain

This comment has been minimized.

Copy link

commented May 4, 2018

Hey, @xoroz, I had the same problem with the WordPress visual editor, But adding the code snippet as the very first condition to evaluated in vcl_recv fixed it for me.

Pasting the same code snippet here for reference.

# Added later to fix visual editor issues
  if (req.url ~ "wp-(login|admin|comments-post.php|cron.php)" || req.url ~ "preview=true" || req.url ~ "xmlrpc.php") {
             return (pass);
   }

Hope it helps.

@Hetann

This comment has been minimized.

Copy link

commented Sep 7, 2018

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

@djeraseit

This comment has been minimized.

Copy link

commented Dec 19, 2018

Watch out for this booby trap!

set resp.http.Access-Control-Allow-Origin = "*";

@danielslyman

This comment has been minimized.

Copy link

commented Jan 4, 2019

Hey there,

could you point me in the right direction in order to make the WP backend work? I'm getting a redirect loop error.

@nnimis

This comment has been minimized.

Copy link

commented Feb 20, 2019

Hi! I would add this for certbot passthrough (for people using Let's Encrypt SSL/TLS):

    if (req.url ~ "^/\.well-known/acme-challenge/") {
        return (pass);
    }
@khanh-nt

This comment has been minimized.

Copy link

commented Feb 25, 2019

Is that VCL compatible with the latest wordpress and woocommerce?

@tschirmer

This comment has been minimized.

Copy link

commented Apr 8, 2019

I've just debugged this for a site I was working on:

please replace the purge method there:

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    return (purge);
}

with

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    ban("req.url ~ /");
    return (purge);
}

The purge is done with

ban("req.url ~ /");

otherwise the purge isn't actually conducted

@che9992

This comment has been minimized.

Copy link

commented Apr 19, 2019

if you have a specific url to login
you have to find this and add the

	# unset cookies from backendresponse
	if (!(bereq.url ~ "(wp-login|wp-admin| your specific url to login here)"))  {
		set beresp.http.X-UnsetCookies = "TRUE";
    		unset beresp.http.set-cookie;
    		set beresp.ttl = 1h;
	}
@toby1kenobi

This comment has been minimized.

Copy link

commented Jun 5, 2019

I've just debugged this for a site I was working on:

please replace the purge method there:

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    return (purge);
}

with

if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
        return(synth(405,"Not allowed."));
    }
    ban("req.url ~ /");
    return (purge);
}

The purge is done with

ban("req.url ~ /");

otherwise the purge isn't actually conducted

Thanks for this, this worked for me too

@matthewjackowski

This comment has been minimized.

Copy link
Owner Author

commented Jun 7, 2019

Hi everyone! OP here, I had no idea so many people were using this as I have moved away from using Varnish for my Wordpress sites in favor of static publishing but am happy to maintain this config.
@tschirmer - Thanks for the update, I'll make that addition!

@matthewjackowski

This comment has been minimized.

Copy link
Owner Author

commented Jun 7, 2019

Hey there,

could you point me in the right direction in order to make the WP backend work? I'm getting a redirect loop error.

Just to be clear, my original intent is to NOT to have the Wordpress BE work through Varnish. There is just so much to think about/can go wrong. Additionally I have security concerns around this because many Wordpress attacks target BE urls.
Instead I would recommend having a completely separate route setup for site admins, operationally it helps so much to have separated activity logs, IP security, etc.

@matthewjackowski

This comment has been minimized.

Copy link
Owner Author

commented Jun 7, 2019

Watch out for this booby trap!

set resp.http.Access-Control-Allow-Origin = "*";

Certainly there is no intent for this to be a "booby trap". The intent is to cache external assets that CORS would otherwise block. Unfortunately I'm not sure how to make this less "scary". Maybe put a url condition on it like this: if (req.url ~ "/fonts/")
I'm open to any suggestions/feedback here.

@matthewjackowski

This comment has been minimized.

Copy link
Owner Author

commented Jun 7, 2019

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

Great comment! This is definitely a rather "opinionated" setting.
I completely agree with your analysis, but my thinking goes along this line:
The goal is to let Varnish quickly give out the cached version to whoever asks for it, I shouldn't really care if it's a browser, a bot whatever (its so simple to impersonate browsers that I'm not sure how valuable it is from a security point of view)
Additionally I really really don't want Varnish keeping separate cached copies for every different User-Agent. I did try to get it to ignore it instead of dropping it altogether, but I couldn't make it work reliably (I kept seeing dups in the cache file).

@toby1kenobi

This comment has been minimized.

Copy link

commented Jul 12, 2019

Why are u removing the User-Agent header from the request?
Maybe i'm missing something but i don't get how that info could be duplicated and in addition to that removing the UA is a loss of control on many security things you can do based on the application that made the request, or at least the most of these.

Great comment! This is definitely a rather "opinionated" setting.
I completely agree with your analysis, but my thinking goes along this line:
The goal is to let Varnish quickly give out the cached version to whoever asks for it, I shouldn't really care if it's a browser, a bot whatever (its so simple to impersonate browsers that I'm not sure how valuable it is from a security point of view)
Additionally I really really don't want Varnish keeping separate cached copies for every different User-Agent. I did try to get it to ignore it instead of dropping it altogether, but I couldn't make it work reliably (I kept seeing dups in the cache file).

For me this line stops WP's visual editor loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.