Though slightly more complex, using a CDN is the most performant option for serving static assets. See the [CDN asset host](cdn-asset-host-rails31) article for more details.
Ruby on Rails applications should use Rack::Cache to efficiently serve assets on the Cedar stack. Proper Rack::Cache usage improves response time, decreases load and is important when serving static assets through your application.
This article will summarize the concepts of caching assets using Rack::Cache and walk you through the appropriate configuration of a Rails 3.1 application and the asset pipeline.
Sample code for this article's [reference application](https://github.com/heroku/rack-cache-demo) is available on GitHub and can be seen running at [http://rack-cache-demo.herokuapp.com/](http://rack-cache-demo.herokuapp.com/)
Rack::Cache is Rack middleware that enables HTTP caching on your application and allows an application to serve assets from a storage backend without requiring work from the main Rails application. Many Ruby web frameworks, including Rails 3+ and Sinatra, are built on top of Rack.
At a high level, Rack works by taking a request and passing it through a series of steps, called middleware. Each Rack middleware performs some action, then passes the request to the next middleware in the rack. Eventually the request is properly formatted and it will reach your application.
Rack is written to be lightweight and flexible. If middleware can respond to a request directly, then it doesn't have to hit your Rails application. This means that the request is retuned faster, and the overall load on the Rails application is decreased.
Rack::Cache has two different storage areas: meta and entity stores. The metastore keeps high level information about each cache entry including HTTP request and response headers. This area stores small chunks of data that is accessed at a high frequency. The entitystore caches the response body content which can be a relatively large amount of data though it is accessed less frequently than the metastore.
Rack::Cache ships with three different storage engines: file
, heap
, and memcache
. Storing data in the file
engine is slower but memory efficient. Using heap
means your process' memory will be used which is quicker but can have an impact on performance if it grows unbounded. Using memcache
is the fastest option though it isn't well suited to store large objects.
Using the metastore with the memcache
storage engine, which allows very quick access to shared meta-data, while using the file
engine for the entitystore and its larger objects results in an efficient and predictable application performance profile and is recommended on Heroku.
To run your application locally and test the Rack::Cache setup you will need to have Memcache installed. You can install it on Mac OSX using a tool such as homebrew.
:::term
$ brew install memcached
At the end of installation homebrew will give you instructions on how to start Memcache manually and automatically on system start.
Heroku recommends using Memcache with the Dalli gem as part of your Rack::Cache
backend. In your Gemfile
add:
:::ruby
gem 'dalli'
After running bundle install
to establish Dalli as an application dependency tell Rails to use the Dalli client for its cache-store in config/application.rb
.
:::ruby
config.cache_store = :dalli_store
Confirm dalli/memcache configuration by starting a local Rails console session and getting/setting a simple key-value.
:::term
$ rails c
> memcache = Dalli::Client.new
> memcache.set('foo', 'bar')
> memcache.get('foo')
'bar'
Once you've configured your application to use memcached it's now time to configure Rack::Cache.
Modify your config/environments/production.rb
environment file to specify the appropriate storage backends for Rails' built-in Rack::Cache integration.
:::ruby
config.action_dispatch.rack_cache = {
:metastore => Dalli::Client.new,
:entitystore => 'file:tmp/cache/rack/body',
:allow_reload => false
}
If not specified, `Dalli::Client.new` automatically retrieves the Memcache server location from the `MEMCACHE_SERVERS` environment variable. Otherwise it will default to localhost and default port.
To allow your application to properly serve, invalidate and refresh static assets several config settings must be updated in config/environments/production.rb
. Allow Rails to serve assets with the serve_static_assets
setting.
:::ruby
config.serve_static_assets = true
Additionally, specify how long an item should stay cached by setting the Cache-Control headers. Without a Cache-Control header static files will not be stored by Rack::Cache
:::ruby
config.static_cache_control = "public, max-age=2592000"
These settings tell Rack::Cache
to store static elements for a very long time. To properly invalidate modified files Rails updates a hash digest in the file name. Enable this approach with the config.assets.digest
setting.
:::ruby
config.assets.digest = true
You also want to confirm that caching is turned on in production.
:::ruby
config.action_controller.perform_caching = true
Since you will use memcache as your Rack::Cache
metastore, you will need to add the Memcache add-on to your application on Heroku.
:::term
$ heroku addons:add memcache
----> Adding memcache to myapp... done, v25 (free)
Deploy the application to Heroku and use the heroku logs
command to view cache output.
:::term
$ git push heroku master
$ heroku logs --ps web -t
You should see cache
entries in your production log-stream. Tailing miss, store
tokens indicate that the item was not found in the cache but has been saved for the next request.
:::term
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] miss, store
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] miss, store
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] miss, store
fresh
indicates item was found in cache and will be served from cache.
:::term
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] fresh
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] fresh
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] fresh
Your Rails 3.1+ application is now configured to cache static assets using Memcached, freeing up dynos to perform dynamic application requests.
If a setting is not configured properly, you might see miss
in your logs instead of store
or fresh
.
:::term
cache: [GET /assets/application-95bd4fe1de99c1cd91ec8e6f348a44bd.css] miss
cache: [GET /assets/application-95fca227f3857c8ac9e7ba4ffed80386.js] miss
cache: [GET /assets/rails-782b548cc1ba7f898cdad2d9eb8420d2.png] miss
When this happens ensure that the Cache-Control header exists by using curl
to inspect asset response headers.
:::term
$ curl -I 'http://rack-cache-demo.herokuapp.com/assets/shipit-72351bb81da0eca408d9bd8342f1b972.jpg'
HTTP/1.1 200 OK
Age: 632
Cache-Control: public, max-age=2592000
Content-length: 70522
Etag: "72351bb81da0eca408d9bd8342f1b972"
Last-Modified: Sun, 25 Mar 2012 01:51:21 GMT
X-Rack-Cache: fresh
The response headers should contain Cache-Control
with the value specific in the config.static_cache_control
setting i.e.: public, max-age=2592000
. Also confirm that you are seeing the X-Rack-Cache
header indicating the status of your asset (fresh/store/miss).
If you modify a file and your server continues to serve the old file check that you committed it to your Git repository before deploying. You can check to see if it exists in your compiled code by using heroku run bash
and listing the contents of the public/assets
directory. This directory should contain the hashed asset file names.
:::term
$ heroku run bash
Running bash attached to terminal... up, run.1
$ ls public/assets
application-95bd4fe1de99c1cd91ec8e6f348a44bd.css application.css manifest.yml
application-95bd4fe1de99c1cd91ec8e6f348a44bd.css.gz application.css.gz rails-782b548cc1ba7f898cdad2d9eb8420d2.png
application-95fca227f3857c8ac9e7ba4ffed80386.js application.js rails.png
application-95fca227f3857c8ac9e7ba4ffed80386.js.gz application.js.gz
Also confirm that the file is listed in Rails' manifest.yml
.
:::term
$ cat public/assets/manifest.yml
rails.png: rails-782b548cc1ba7f898cdad2d9eb8420d2.png
application.js: application-95fca227f3857c8ac9e7ba4ffed80386.js
application.css: application-95bd4fe1de99c1cd91ec8e6f348a44bd.css
If the file you're looking for does not show up try running bundle exec rake assets:precompile RAILS_ENV=production
locally and ensure that it is in your own public/assets
directory.
Great stuff. Few comments:
heroku addons:add memcache
part of the process inline here instead of linking out to http://devcenter.heroku.com/articles/memcache#using_memcache_from_ruby. The pattern here is to include the link for more background on Memcached in a callout: http://devcenter.heroku.com/articles/writing#calloutsMEMCACHE_SERVERS
in a.env
etc...). Minus installing memcached locally, I think we should cover it. Many of our tutorials have the pattern of describing code, running locally and deploying - we should try and mimic.MEMCACHE_SERVERS
env var? If not - how can you just sayDalli::Client.new
in the rack-cache config?