Configure the Expires header for Rails under nginx

The images, CSS and JavaScript files served up by your Rails application can usually be cached by the web browser, rather than being downloaded every time the browser loads a page on your site. Rails gives you some help here, but it doesn't happen automatically -- you need to configure your web server to set the HTTP Expires header.

This article explains how to configure the expires header for a Rails application running behind Nginx.

Nginx is popular amongst Rails developers for fronting a cluster of mongrel web servers. I'll not say much more about it, on the assumption that this article probably won't be of much interest to you if you're not already using it.

So how do we speed your Rails site up?

When a web browser downloads a web page for the first time it also retrieves copies of the images, CSS and JavaScript files that are required to display it. On subsequent requests it will typically make a new connection to the web server to verify whether each of these files has been updated. When you know that the files won't have changed you can gain a significant performance increase by telling the browser when it first downloads the file that it doesn't need to check it again.

More information on the Expires header (and other great performance enhancing tips) can be found amongst Yahoo's Best Practices for Speeding Up Your Web Site.

Files that don't change between requests are known in the Rails community as "static assets". When running in production mode your Rails application will append all URLs for static assets with the file's timestamp (in seconds since the 1 January 1970). Here's an example:

http://effectif.com/stylesheets/blog.css?1221178271

If we were to edit the file and redeploy the application the timestamp would be updated, and the browser would realise that it needed to download a new copy of the file.

The Rails documentation for AssetTagHelper covers how to configure a far future expires header under Apache (search for "FilesMatch" to find it).

The equivalent nginx configuration looks like this:

if ($request_uri ~* "\.(ico|css|js|gif|jpe?g|png)\?[0-9]+$") {
    expires max;
    break;
}

Note that we're matching suitable file types, but only setting the expires header for files that have a timestamp appended.

Here's the configuration again, in the context of a location directive:

location / {
    root /var/apps/myapp/current/public;
    proxy_set_header  X-Real-IP  $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect false;

    if ($request_uri ~* "\.(ico|css|js|gif|jpe?g|png)\?[0-9]+$") {
        expires max;
        break;
    }
    if (-f $request_filename) {
        break;
    }
    if (-f $request_filename/index.html) {
        rewrite (.*) $1/index.html break;
    }
    if (-f $request_filename.html) {
        rewrite (.*) $1.html break;
    }

    proxy_pass http://mymongrels;
}

Testing your configuration

It's all well and good sticking the config in nginx and restarting it, but it's nice to know that the stuff works, right? If you're not already using it, get a copy of Firefox and install Firebug. Then install Yahoo's YSlow plugin for Firebug.

I recommend that you read Yahoo's introduction to YSlow to learn how to use it. You can check whether the expires header is set for all the assets required by your web page using the Components view, a screenshot of which I've cheekily purloined here:

YSlow copmonents view, showing the value of the expires header

I love feedback and questions — please feel free to get in touch on Mastodon or Twitter, or leave a comment.