Albert Callarisa Roca

Tech blog

Varnish ESI with Rails Part 1

14 Feb 2013

This last few days I spent some time adding Varnish in front of a Rails app. My previous experience with Varnish is basically the old times in Heroku, a magic non-configurable Varnish layer on top of your app. This time we're setting up our own infrastructure, including Varnish.

Some of the details about the application and its caching requirements that might affect the Varnish setup are:

  1. The app itself will set cache headers depending on the page.
  2. The page content will change depending on the value of some headers.
    For example two requests, one with X-SOMETHING: a and another one with X-SOMETHING: b will not have the same content, so will not share cache. (see Trying to understand the “Vary” HTTP header StackOverflow thread)
  3. Some elements in the page might be user specific, for example the header with the user's profile picture.

First steps

The first thing to do is get Varnish, process that I will not describe here. In order to run Varnish we need to create the configuration file, see VCL documentation.

Here I paste a sample VCL file with some comments. I took most of it from other examples.


backend default {
    .host = "yourapphost.com";
    .port = "80";
}
sub vcl_recv {
  set req.grace = 10m;

  # This rule is to insert the client's ip address into the request header
  if (req.restarts == 0) {
    if (req.http.x-forwarded-for) {
      set req.http.X-Forwarded-For = req.http.X-Forwarded-For +", "+ client.ip;
    } else {
      set req.http.X-Forwarded-For = client.ip;
    }
  }

  # Don't cache requests other than GET or HEAD
  if (req.request != "GET" && req.request != "HEAD") {
    return (pass);
  }

  return(lookup);
}

sub vcl_pipe {
  set bereq.http.connection = "close";
  return(pipe);
}

sub vcl_fetch {
  set beresp.grace = 10m;
  if (beresp.http.Cache-Control ~ "max-age") {
    unset beresp.http.Set-Cookie;
    return(deliver);
  }
  return(hit_for_pass);
}

sub vcl_deliver {
  # The below provides custom headers to indicate whether the response came from
  # varnish cache or directly from the app.
  if (obj.hits > 0) {
    set resp.http.X-Varnish-Cache = "HIT";
  } else {
    set resp.http.X-Varnish-Cache = "MISS";
  }
}
  

With that simple Varnish script all is ready to run varnish.

Run varnishd -a 0.0.0.0:8080 -s malloc,500M -F -f varnish.vcl to run it locally for testing. This will basically run Varnish on port 8080 with 500M cache in memory using the config file varnish.vcl. Any http request to the port 8080 will be handled by Varnish, either delegating it to the webserver or returning it from its cache. You can check the response headers to validate it is working.

Integration with Rails

At this point Varnish is already working on top of the rails server.
The Varnish configuration needs the webapp to set a max-age in the Cache-Control header in order to cache it in Varnish.

To do so, put the following line somewhere in the code of your action.


class PostsController < ApplicationController
  def index
    # Your index action code
    # ...
    expires_in 30.seconds, public: true
  end
end
  
This will tell Varnish to cache the whole response for 30 seconds. You can validate sending requests through Varnish and checking the X-Varnish-Cache header value.

Conclusion

This is all for this part. So far this is too basic, quite straight-forward. This is not really the most interesting part of our setup. Next parts will include the Vary header configuration and the user-specific content using ESI.