Multiple-site configuration for Varnish Server

Recently, I started looking into building a Varnish server that could handle multiple websites, possibly running multiple frameworks. I knew that it was possible to include extra VCL’s based on hostname as seen on the Varnish Software blog, but when I tried to implement that in my config, I kept on getting errors. This was because Varnish won’t let you use an if()statement outside of one of the action blocks. On digging deeper, I found that people recommended using that structure inside a block like the vcl_recv block, and simply not using additional full VCL’s.

This presented a bit of a problem for me, as I really, really needed to be able to configure more than just the vcl_recv block on a per-site basis. Fortunately, there’s a way to do that without maintaining multiple VCL files per site.

Since I host multiple sites on one IP, I needed to sort the connections by host header in order to cache appropriately. This meant making sure that I could check the host header consistently across sites.

The cool thing about Varnish is that it compiles your VCL’s when it loads, which means that you can have multiple sub blocks that are compiled in order. So, I took advantage of that to run some global processing in my default.vcl; I added the following lines to the vcl_recv block.

set req.http.Host = regsub(req.http.Host, "^www\.", "");
set req.http.Host = regsub(req.http.Host, ":80$", "");

I also added the same lines to the vcl_backend_response block, only changing req to bereq.

That code meant that I could easily normalize my host headers across multiple requests and not have to worry about serving an incorrect page, or caching content twice. However, I still needed to make sure that I was caching different domains using different sets of rules.

To do this, I created a conf.d folder in /etc/varnish as a place to store my per-site VCL’s. In there, I put full VCL’s, with four minor changes.

First, I added the following block to the top of the VCL:

acl domainname_purge {
"localhost";
"127.0.0.1";
"(domain of the website)";
"(internal network IP for the website)";
}

I also enclosed the contents of the vcl_recv, vcl_pipe, vcl_pass, vcl_hash, and vcl_req blocks with a conditional as follows:

if(req.http.host == "domain.com") {

In the vcl_backend_response block, I instead used this conditional:

if(bereq.http.host == "domain.com") {

Finally, I added another conditional to the vcl_recv block:

if (req.method == "PURGE") {
# If not allowed then a error 405 is returned
if (!client.ip ~ domainname_purge) {
return(synth(405, "This IP is not allowed to send PURGE requests."));
}
return (purge);
}

Between these four changes, I was looking to ensure that not only would Varnish cache differently based on each site, but also that a website could only purge its own cache on request.

After adding some include "conf.d/domainname.vcl"; lines to default.vcl (placed after my global processing rules, of course!), I simply ran systemctl reload varnish and voila, I had myself a working server. Testing showed that indeed Varnish was processing different sites using their individual VCL’s.

There’s other stuff you can do with this setup, too; for example, if you want to add global rules for processing if no host header matched the request, you can add another vcl_recv block after your includes. I used it to just serve a standard 404 page –return(synth(404, "File not found")); – but you can pretty much do whatever you want.

This seems to be the most maintainable method to serve multiple websites through Varnish from my point of view, but did you come up with a different strategy that you think works better? Let me know in the comments!

(Note: All code snippets are written to work on Varnish 4. Trying to run most of this stuff using a previous version of Varnish will probably break things really hard.)

Leave a Reply