Rails CDN and ActiveStorage
When serving assets and attachments from Rails and ActiveStorage, a CDN is a great way to reduce load on your web server and speed up content delivery to your users.
The ideal setup here is to serve your assets from your app, set a high cache-expiration, and serve your assets through a CDN.
Setting up your Asset CDN
For a CDN, I recommend Amazon CloudFront. You’ll want to create a web distribution which points at the root of your app. Heroku has a good guide on that.
Once it’s set up, set an environment variable1 (e.g.
ASSET_HOST) to the distribution domain name and configure your
asset_host. Also, set a long expiration for your assets. In
config/environments/production.rb 2 3:
Once you’ve configured the above, your production assets should serve from your CloudFront CDN.
For more information, on serving assets, caching, and cache invalidation, see the Rails Guides.
ActiveStorage Attachments served through a CDN
When you serve attachments from a cloud storage service (I’m using S3), it will be coming from a different domain than your app. One solution to this would be to stream the file contents through your app.
I opted for creating a second CDN distribution that sits in front of the cloud storage provider that ActiveStorage serves for its 302 redirect 4. This does require a little configuration. Thankfully nothing needs to be monkey patched.
- Create a second CloudFront web distribution that points to your S3 bucket and point your
ATTACHMENT_HOSTenv variable at it (prepended with https://).
lib/active_storage/service/s3_directory_service.rb, create the service in this gist.
config/environment/production.rb, set ActiveStorage’s
# Tell our CDN and browser to cache attachments for a year. config.active_storage.service_urls_expire_in = 1.year
In config/storage.yml, configure the service and default
cache_controlfor uploaded attachments by adding the upload key 5:
amazon: service: S3Directory # ... upload: cache_control: 'public, max-age=31536000'
When linking to the attachments, use the
rails_blob_urlhelper like so: 6
rails_blob_url user.avatar, host: ENV['ASSET_HOST']
(For variants, you’ll need to call
Now your ActiveStorage attachments will serve through the two CDNs: the first request will hit Rails’ Representations or Blob controller (through the Asset CDN) which will redirect to the service url for the attachment (through the Attachment CDN). If this attachment has been served to a previous user, no request will hit your web server as CloudFront will have it in its cache. If that user has requested the attachment before, it well be served from the browser’s memory and no request will be made.7
If you’re using Heroku Review Apps, you’ll want to conditionally use
You may also want to set your mailers’ asset host.↩
See Rails’ Representation and Variants controller to see how that works.↩
If you already have ActiveStorage attachments uploaded in production, you can make them public and add a cache-control header by using aws cli tools.↩
You may want to create a helper method to make this a little cleaner.↩
The same is true for your assets and the Asset CDN.↩