October 3rd 2011

Do the asset serving dance on Heroku Cedar with Rails 3.1 and CloudFront

The asset pipeline in Rails 3.1 and Heroku's Cedar stack adds a few new things to think about when serving static assets from your apps. Heroku has removed Varnish and nginx from the the cedar stack which means that static files served from a app won't get cached or gzipped automagically. One way to go is to use Amazon's S3 as asset host together with the asset_sync gem. However there are a few problems that can occur when Heroku run rake assets:precompile during the deploy process.

With these facts you might realize that you want to put your compiled assets on a content delivery network like Amazon's CloudFront so the assets can be served fast and your dynos don't have to be bothered with serving and compiling assets.

How do we solve this in a good way?

First create a new CloudFront distribution. A great thing with CloudFront is that you can set any domain as the source for the distribution. You do this for your Heroku hosted app by specifying the apps domain as the ”Custom Origin” when creating a new distribution. With this set CloudFront don't need a S3 bucket, instead it will mirror the assets from the custom origin domain. When the distribution gets a request for a file that it don't have mirrored yet it will just issue the same request to your app, cache the result and return it. This also makes it possible to serve gzipped versions of your assets as CloudFront forwards the clients request headers and rack can serve gzipped versions of your assets if the client supports it (more on this further down).

Add the new distribution's domain name (or CNAME if you specifed one) as the the asset host for your app in production environment:

# config/environments/production.rb
..
config.action_controller.asset_host = "http://aabbccdd.cloudfront.net"
..

This will make rails add the specified asset host to the generated url when you use stylesheet_link_tag or similair helpers in production. For example if you do

<%= stylesheet_link_tag 'application' %>

you will get

<link href="http://aabbccdd.cloudfront.net/assets/application-388a2a900c29a176d20c18ed000f77fa.css" media="screen" rel="stylesheet" type="text/css" />

Great, but we are not quite done yet because Heroku will automatically run the rake task to compile the assets when your app is deployed. This is a problem because if the files that CloudFront requests exists on disk then they will not be served from your rails app but from the disk, so we won't get the correct cache headers and we won't be able to serve gzipped versions of the assets.

What we need to do

1. Disable the assets precompile rake task

There is two ways to do this. By overriding the assets:precompile rake task:

# lib/tasks/disable_assets_precompile.rake
Rake::Task['assets:precompile'].clear
namespace :assets do
  task :precompile do
    puts '* rake assets:precompile has been disabled (lib/tasks/disable_precompile.rake)'
  end
end

You can also add a empty file called public/assets/manifest.yml to your project, if Heroku detects this file it will not run the assets:precompile task on deploy. Both these ways are kind if hackish, so choose the one that suits you best.

2. Enable on-the-fly asset compiling in production

As we now need to serve the assets through the rails stack when CloudFront makes it's first requests we need to enable asset compiling in the production environment, this is done by setting config.assets.compile to true in your production settings:

# config/environments/production.rb
..
config.assets.compile = true
..

3. Enable serving of gzipped content

As Heroku has removed nginx from the cedar stack we have to do the gzipping by ourselves, this is done by adding a rack middleware called Rack::Deflater. It's part of Rack core so it's really easy to add, just update your config.ru file in the root of your rails project to look like this:

require ::File.expand_path('../config/environment',  __FILE__)
use Rack::Deflater
run MySuperApp::Application

Done, deploy your app and celebrate with beer. With this setup you get a geo-aware, fast and cachable way to serve your Rails 3.1 assets.

Thanks to @joeljunstrom for the proofreading!

Endorse arvida on Coderwall
blog comments powered by Disqus