In Rails versions prior to 6.1, Rails Active Storage only served files from an expiring URL that redirected to the selected service. This made it impossible to serve the files via a CDN. There were many workarounds to this problem, and fortunately Rails introduced the new proxy features for serving files from Active Storage in Rails 6.1.
The proxy feature provides a permanent URL to an asset through your Rails application instead of an expiring URL. This enables you to put a CDN between your application and the browser. When a user requests an asset from your site the architecture looks like this:
The first time an asset is requested, it will be served by your application. On all subsequent requests, it will be served from the CDN. To test this, access a file served by Active Storage. Here is one as an example: View this link to see a proxied URL.
Open the “Network” tab in developer tools and request the resource again (i.e. refresh the page). You can see HIT in the cache headers, showing the file was served from a CDN, not from your application.
A lot of the workarounds the community was doing before Rails 6.1 involved creating a direct URL using the object key and the desired CDN domain. There were a few problems with this.
First of all, if you were serving public files from S3, the storage providers usually required the domain name and the bucket name to be identical. This really became a problem if you were using wildcard subdomains.
Also, it made it tricker to switch storage providers. It was possible, but there were necessary DNS changes and the buckets still had to conform to the specified naming conventions.
With the new proxy feature switching storage providers is as easy as updating
storage.yml in your Rails application. You can also now easily use wildcard subdomains and have more freedom in naming your buckets.
Assuming you have Active Storage set up and you’re trying to add a CDN, all you need to do is update your routing and serving of files.
storage.yml, add the
public:true setting to your configuration if you haven’t already done so:
In a standard Active Storage configuration, you serve the file using
<%= image_tag(@user.avatar) %> (for example).
This provides you an expiring URL that redirects to your storage service. You’ll notice the URL typically has the word “redirect” in the path.
Using the default Active Storage serving service, the URL will look like the one below:
Notice the blobs/redirect string in the URL. If you click an expiring URL it will redirect to your bucket and eventually expire. Which means it can’t be cached by a CDN.
To use a CDN, you need to change the URL to be permanent. One way to do this is to modify the
routes.rb file and use the route directly.
In your view you can now use
<%= image_tag cdn_image_url(@user.avatar) %>. If you look at the generated URL, you will see it now contains the string
blob/proxy and when you click it you are not redirected to the bucket/key endpoint:
If you don’t have your CDN set up at this point, you can use your app host in the
CDN_HOST variable, and the assets will still resolve correctly.
With the setup just described, you can still use the redirect URL alongside the
cdn_url as needed. The following routes both work. The first produces the redirect URL, and the second produces the proxy URL.
<%= image_tag (@user.avatar)%>creates a redirect URL
<%= image_tag cdn_image_url(@user.avatar) %>creates a proxy URL
If you want to proxy all your files, you can add an initializer to your application. Create an
active_storage.rb file in
config/initializers with the following:
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_proxy
You can then remove the
cdn_routes created in the previous step, and all files will use the proxy URL. So in this case with one additional line in the configuration a standard
<%= image_tag (@user.avatar), width: 500, height: 500 %> will produce a proxy URL.
Another option is to use route helpers directly. With no
active_storage.rb configuration and no
cdn_image routes defined, you can use
<%= image_tag rails_storage_proxy_path(@user.avatar), height: 100, width: 100 %> directly in your view to generate the proxy URL.
This helper accepts parameters, so you can specify your CDN host if desired:
rails_storage_proxy_url(@user.avatar, host: Rails.application.credentials.dig(:CDN_HOST), protocol: “https")
The architecture for the complete DNS and CDN setup for the application will look something like this:
- Select a CDN. For this article, we’ll use the Expedited CDN provided in the Heroku application store. We’ll also deploy the application with Heroku. If using Heroku, add your custom domain to your Heroku application. This is available on the Settings tab of your Heroku application.
- Add the target to your DNS provider. After you add a custom domain with Heroku, Heroku will provide you a DNS record. Add this record to your domain's DNS configuration. Here’s an example of what that might look like with NameCheap as a DNS provider:
- Complete the Setup of Expedited CDN or the CDN of your choice. Typically at this point, you will be prompted by your CDN provider to add additional DNS records to start using the CDN. Expedited CDN with Heroku will provide a set of prompts to configure the service. The first step is to select the Heroku domain you would like to use with the CDN. Follow the prompts to add additional DNS records.
Wait for the DNS to resolve. After the DNS has resolved, complete the CDN setup. Add the
CDN_HOSTas an environment variable on your production environment.
That's it! Once all the DNS changes have propagated, the setup is complete. The very first time a file is requested, the request will go through your Rails application, and subsequent requests will be served via the CDN.