How to serve assets via Amazon S3 & Cloudfront for Rails 4 with Paperclip

We are working hard to get our new project up and running. It will be a website aimed for food lovers built with Ruby On Rails.

In order to make the server super-snappy we have decided to use Amazon S3 for file storage and have a Amazon Cloudfront CDN. This all because our server should only concentrate on serving views, not assets. Therefore we also wan’t the Rails assets such as javascripts, stylesheets, images and fonts to be served via the CDN and not our core app. This little guide will explain how to set the whole thing up.

We use Capistrano to deploy our project and fortunately there is a sweeeeet gem called asset_sync that is designed to help you sync your assets to other places, such as Amazon S3.

Synchronizing assets

In order to sync the assets from your Rails app to S3 we used the gem asset_sync. This cool gem will hook up on the assets:precompile call and then sync your assets to S3 automagically. Since assets:precompile is always executed when deploying with Capistrano it will work straight out of the box. The best part, the gem is super-easy to install!

In your Gemfile just put

gem 'asset_sync'

preferably you put it in the :assets group.

… Then run bundle install

In order to get the assets to sync to the right place you need to setup your credentials. I found that when using Capistrano, although you maybe wan’t to use a Yaml-config, it didn’t work for me. So the fix was to use a initializer instead. So to generate a template file you just run:

rails g asset_sync:install --provider=AWS

Change the information in the file to correspond with your user. I strongly recommend that you make an Amazon IAM user in order to not use your super-user account.

IMPORTANT: Since we are using an initializer, you also need to wrap the file created for you with:

if defined?(AssetSync)
  .... Original content in file with config options
end

.. otherwise it will crash since we put the gem in the assets group

In production.rb (or application.rb if you wan’t to use it in dev-mode) you should check that the following config options are correct (otherwise add them)

config.assets.enabled = true
config.assets.digest = true
config.action_controller.asset_host = "http://BUCKETNAME.s3.amazonaws.com"
config.assets.initialize_on_precompile = true

The last line is used so that our initializer actually is available when the assets are precompiled. Otherwise it will crash. If you are using another region than the standard on S3 be sure to use the correct URL for that region, i.e. http://BUCKETNAME.s3-eu-west-1.amazonaws.com

This should be enough to get your assets synced to your S3 bucket, let’s move on to the next part which is to get the CDN up. You should now be able to deploy and verify that the assets are indeed served from S3 and not the app.

Hook up your CloudFront CDN to serve the assets

First off, you need to setup a few DNS entries on the site. Remember, it can take a few hours for the DNS entries to be available so you might wan’t to go grab a lunch or something when you have set the whole thing up ;) You will need 4 CNAME entries, each named with your chosen subdomain + 0-3 and they all should point to your CloudFront CDN URL.

What you need to do is to log into your Amazon account and add a new CloudFront distribution. Point your distribution against your S3 bucket. Remember: It will take a little while before the CDN is actually created, look in the management console that the status has changed from InProgress to Deployed.

In the option for CNAMES that appears when you create the distribution you should put in your created entries

static0.ratherunique.se
static1.ratherunique.se
static2.ratherunique.se
static3.ratherunique.se

We are almost done here, what you need to do now is to point out for Rails that the assets is supposed to be served from another location, so in production.rb just put

config.action_controller.asset_host = "http://static%d.ratherunique.se"

The %d will make Rails randomize a number between 0 and 3 that will be used. This way your browser can have more requests simultaneously. Sweet huh!

If you deploy now (and are sure that your CloudFront is active, and your subdomains are available in the DNS) you should see that the assets are now being served from your custom domain name via the CDN. Pretty damn awesome if you ask me.

Hooking up PaperClip to the CDN

Next step is to hook up PaperClip so that it also will be served from the CDN. I will not go into details on how to install PaperClip but here is the setup you need to get it working with the custom domains.

In production.rb you should add

config.paperclip_defaults = {
    :storage => :s3,
    :s3_host_name => 's3-eu-west-1.amazonaws.com',
    :url => ":asset_host",
    :s3_credentials => {
      :bucket => "YOURBUCKET",
      :access_key_id => "YOUR_ACCESS_KEY_OR_USE_ENV_VAR",
      :secret_access_key => "YOUR_SECRET_ACCESS_KEY_OR_USE_ENV_VAR"
    }
  }

You also need to set the path on the model. I.e.

has_attached_file :photo, 
										:styles => 
											{ 
												:medium => "300x300>", 
												:thumb => "480x270#",
												:normal => "642x361#"
											},
											:path => "recept/bilder/:id/:style/:filename"

One thing to remember though, if you call @model.photo.url you will get the relative URL to the image. It is therefore important to use image_tag when displaying images to get it with the correct asset host.

<%= image_tag @model.photo.url %>

That’s it, easy does it! Enjoy your super-snappy web app with assets served from CDN you happy little camper :)