Cloudflare R2 with Active Storage

By Exequiel Rozas

- May 12, 2025

Handling file uploads in Rails applications has never been easier.

With Active Storage, we can be up and running in a matter of minutes with local uploads and, with some extra effort, we can get cloud uploads rapidly.

In this article, we will learn how to set Cloudflare R2 with Active Storage to use it as our cloud provider and also use Cloudflare's CDN so we get fast

Why use R2 for object storage?

R2 is Cloudflare's object storage offering. It's relatively recent, as it was officially released in September 2022.

If you've added cloud storage to a Rails application, you've probably used something like AWS S3, which seems to be the most common choice for developers nowadays.

But, an alternative like R2 has the following advantages over S3:

  • No egress fees: this is what you pay for retrieving the stored data.
  • S3 compatibility: its API is fully compatible with S3, which means we can reuse our own or third-party integrations with the AWS service. Migrating out of S3 or any S3-compatible service like Digital Ocean Spaces is easier.
  • Globally distributed: our stored objects can be accessed from the more than 125 edge locations that Cloudflare has. This means that we don't need to add a CDN on top of it, like we did in the CDN with Active Storage article. Note that the Cloudflare offers 6 bucket locations: Asia-Pacific (apac), Eastern Europe (eeur), Eastern North America (enam), Oceania (oc), Western Europe (weur) and Western North America (wnam), but the objects get distributed along their global network when they're accessed by users.
  • Simplified pricing: R2 offers a simplified pricing where we get 10 GB per month of free storage space plus 10 million “Class B” operations which read existing state and 1 million “Class A” operations which mutate state. After we pass those limits, each GB of storage costs $0.015, each million of reads costs $0.36 and each million of mutations costs $4.5.
  • Better developer experience: working with the Cloudflare dashboard feels like a breeze compared to AWS. The interface is cleaner and more understandable, which results in a better experience overall.

The pricing differences between Cloudflare R2 and a competing service like S3 are probably not as noticeable if your application doesn't have many users or if file uploads are not a main part of it. Consider that when making the decision of whether to use this alternative or not.

Application setup

We will build a simple application where users can create posts and upload cover images, just for the sake of demonstrating that the integration works.

Let's start by creating the application:

rails new cloudflare_r2 --css=tailwind

Let's also install Active Storage so it copies the required migrations for blobs, attachments, and variants:

bin/rails active_storage:install

Next, let's create a simple Post model that with a title, a body and an attached cover:

bin/rails generate scaffold Post title body:text cover:attachment

Which will generate a scaffold with the associated cover:

class Post < ApplicationRecord
  has_one_attached :cover
end

Finally, let's run the pending migrations

bin/rails db:migrate

After some work on the scaffold visuals using Tailwind and using our own Marksmith markdown editor for Rails we get the following:

Post form interface

R2 configuration

The first step of the process, given that you have a Cloudflare account, is to create a new bucket.

From the Cloudflare dashboard, we have to navigate to the “R2 Object Storage” link, which currently sits below the “Storage and databases” option on the sidebar.

After navigating to it, we should see a list of the buckets associated with our account. Logically, if we haven't created one, yet it should be empty.

Cloudflare R2 object storage overview

Next, we click on the “Create bucket” button, and then we should see the following:

Bucket creation flow in Cloudflare R2

We just have to give our bucket a name, avo-r2 in our case, and change the location and storage class if we need to. Otherwise, we can leave the default options.

After we click on the “Create bucket” button, we should get redirected back to the bucket overview, but this time we should see the recently created bucket on the list:

R2 bucket overview after bucket creation

The next step is to create an API token to use with Active Storage when uploading files. To create a token, we have to click on the “{} API” button, which will show a dropdown:

R2 create an API token dropdown

Then, we click on the “Manage API tokens” link, which should take us to the following screen:

R2 API tokens list view

Next, we click on the “Create Account API Token” button because we just need an API key associated with our account for now, and we should see:

R2 Account API tokens view

Here, we can configure the following:

  • Token name: we can give the token a name if pertinent.
  • Permissions: because we want to handle uploads from our Rails app, we have to give “Admin Read & Write” permissions to this token.
  • TTL: set to “Forever” by default, but we can change it if we need the token to expire earlier.
  • Client IP Address filtering: we can limit the IP address that can use the token. Unless you want to filter the access to the token, you can safely leave this empty.

Next, after confirming the setup values, we should see something like this:

R2 API token values screen

This provides us with access to four different values:

  • Cloudflare API token: we can use this token to authenticate requests to the Cloudflare API.
  • Access Key ID and Secret Access Key: these credentials are used to replace AWS credentials when using S3-compatible clients, like we will.
  • Jurisdiction-specific endpoint: this is used when we have jurisdictional restrictions needed to meet data requirements.

Finally, we need to retrieve the ID associated with our Cloudflare account.

There are two ways to retrieve it: it's in the URL for most parts of the dashboard: https://dash.cloudflare.com/#{ACCOUNT_ID}/r2/overview for example.

Otherwise, you can navigate to your buckets settings and check the header of the General section, where you will be presented with something like this:

Retrieving our account ID in Cloudflare

The account_id corresponds to the blurred section of this image.

Now that we have our credential's setup, let's configure Active Storage:

Develop apps 10 times faster with Avo

Develop your next Rails app in a fraction of the time using Avo as your admin framework.

Start for free today

Active Storage configuration

Now that we have the tokens, let's configure Active Storage so we can upload files to our R2 bucket.

Let's start by opening an editor to keep our tokens using Rails credentials:

# Vim
EDITOR=vim bin/rails credentials:edit --environment=development
# VS Code
EDITOR='code --wait' bin/rails credentials:edit --environment=development
# Cursor
EDITOR='cursor --wait' bin/rails credentials:edit --environment=development

In there, let's remove the placeholder values and replace them with something like this:

cloudflare:
  account_id: YOUR_ACCOUNT_ID
  r2:
    account_api_token: YOUR_ACCOUNT_LEVEL_TOKEN
    access_key_id: YOUR_ACCESS_KEY_ID
    secret_access_key: YOUR_SECRET_ACCESS_KEY
    endpoint: YOUR_JURISDICTION_SPECIFIC_ENDPOINT
    bucket: YOUR_BUCKETS_NAME

Now, we can safely access these values in the storage.yml file, which is committed to our repository by default:

# config/storage.yml
cloudflare:
  service: S3
  endpoint: https://<%= Rails.application.credentials.dig(:cloudflare, :account_id) %>.r2.cloudflarestorage.com
  access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :r2, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :r2, :secret_access_key) %>
  bucket: <%= Rails.application.credentials.dig(:cloudflare, :r2, :bucket) %>
  region: auto

The next step is to add the official aws_sdk_s3 gem, which Active Storage uses to upload files to S3-compatible APIs. Make sure to check the latest version of the gem whenever you're implementing this feature:

# Gemfile
gem "aws-sdk-s3", "~> 1.185", require: false

Make sure to have the image_processing gem if you will be using Active Storage's variants:

gem "image_processing", "~> 1.2"

Then, we can tell Active Storage to use the cloudflare service we defined in storage.yml:

# config/environments/development.rb
config.active_storage.service = :cloudflare

Now, after running bundle install and restarting our server, we should be set to go with file uploads.

However, if we try to upload a file with our current configuration, we will receive the following error: Aws::S3::Errors::InvalidRequest with the message You can only specify one checksum at a time.

Checksum error with AWS SDK S3 integration

The error originates from the ActiveStorage::Blob.create_and_upload! method, which sends two checksums, something that's allowed by AWS but rejected by R2 in this case.

This problem seems to happen on newer versions of the AWS SDK, so setting a previous version is an alternative, but the easiest fix is to modify the configuration in storage.yml by adding the following:

request_checksum_calculation: "when_required"
response_checksum_validation: "when_required"

Now, after restarting the server, we can upload a file to R2:

Checking our R2 dashboard, we can see that the object was successfully uploaded:

Successful file upload in R2 dashboard

Everything is working now, but with this configuration, we won't be able to handle direct uploads like we did in the S3 uploads with Active Storage article.

Let's implement the feature with R2 as well:

Have you ever wondered why some gems have the require: false argument in the Gemfile? As its name states, require: false prevents the gem from being loaded automatically when our Rails app starts up. This is done to avoid loading large gems until the point they're actually used. If we explore the activestorage code, the gem is explicitly required in the ActiveStorage::Service:S3Service class.

Direct uploads

Sometimes, especially with customer-facing apps, we want the users to upload files directly from their browser to improve the perceived experience and save server resources.

To achieve this, we need to request a signed URL from our server, which has access to the Cloudflare keys, with information about the file we want to upload.

Then, the server responds with a signed URL, which we use to make a request to Cloudflare from the browser.

If the upload is successful, the cloud server returns information about the uploaded file, which we use to persist to our database.

The flow works like this:

Direct file upload to the cloud flow

To enable direct uploads, let's configure CORS in the Cloudflare dashboard:

Configuring CORS

CORS means Cross-Origin Resource Sharing, which is a way to allow a server to indicate the origins from which a browser should permit resource loading.

In other words, when our browser tries to upload a file to R2, the request is considered cross-origin, and it's not allowed.

To set CORS, we have to open our bucket's setting page and then navigate to the CORS Policy section, then click on the + Add button:

CORS Policy option in Cloudflare's dashboard

Then, we will be presented with a screen where we can edit our CORS policy, which by default is:

[
  {
    "AllowedOrigins": [
      "http://localhost:3000"
    ],
    "AllowedMethods": [
      "GET"
    ]
  }
]

We only have localhost:3000 GET requests, but those are not enough for direct uploads, so let's add the correct headers and methods:

[
  {
    "AllowedOrigins": ["http://localhost:3000"],
    "AllowedMethods": ["GET", "POST", "PUT"],
    "AllowedHeaders": ["Content-Type", "Content-Md5", "Content-Disposition"],
    "ExposeHeaders": ["Origin", "Content-Type", "Content-Md5", "Content-Disposition"],
    "MaxAgeSeconds": 3600
  }
]

We're adding POST and PUT requests, setting the AllowedHeaders which are the headers our application can send and setting the ExposeHeaders which are the headers that are exposed in the response from R2 and MaxAgeSeconds which defines how long the results of the preflight request can be cached.

If you're using a custom development domain or deploying the app to production, don't forget to add your domains to the AllowedOrigins array and try to keep those contained to origins you'll actually use.

CORS preflight requests are OPTIONS requests that use specific headers like Access-Control-Request-Method, Origin and Access-Control-Request-Headers to see if the server allows the request conditions by checking if the response matches the allowed request methods and headers and then proceeds with the actual request which, in our case, would be the actual file upload to the presigned URL we got from Active Storage

Direct uploads with Active Storage

Now that we have our CORS policy correctly setup, we can easily add direct uploads.

The first step is to install the @rails/activestorage JavaScript dependency:

bin/importmap pin "@rails/activestorage" # If using Import maps
yarn add @rails/activestorage # If using JavaScript bundlers

Next, we have to import the ActiveStorage JS code and run ActiveStorage.start(), we can do that in application.js or in any other part of the app:

import * as ActiveStorage from "@rails/activestorage";

ActiveStorage.start();

Then, we just have to pass direct_upload: true to our file field form helper:

<%= form.file_field :cover, direct_upload: true %>

Now, we get direct uploads after we submit the form, but we won't notice any difference, so let's add an event listener to get an alert when the direct upload finishes:

window.addEventListener("direct-upload:end", (evt) => {
  alert("Direct upload started", evt)
})

TL;DR;

Cloudflare R2 is an S3-compatible cloud object storage service which provides some advantages regarding S3:

  • No egress fees.
  • Out of the box CDN integration.
  • Simpler pricing
  • A better experience with their dashboard.

Being compatible with S3, it means that we can use Active Storage just like we with S3 without making any further adjustments or writing any custom code.

The first step, provided we have a Cloudflare account, is to create the bucket we will use and gather the account_id, the bucket name, the access_key_id and secret_access_key and then safely store them with Rails credentials or environment variables if you prefer them.

Then, we add the following service to storage.yml:

cloudflare:
  service: S3
  endpoint: https://<%= Rails.application.credentials.dig(:cloudflare, :account_id) %>.r2.cloudflarestorage.com
  access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :r2, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :r2, :secret_access_key) %>
  bucket: <%= Rails.application.credentials.dig(:cloudflare, :r2, :bucket) %>
  region: auto
  request_checksum_calculation: "when_required"
  response_checksum_validation: "when_required"

And that's it, we can have simple file uploads to R2 without any extra effort.

But, if you want to allow direct uploads, we have to configure CORS:

[
  {
    "AllowedOrigins": ["http://localhost:3000"],
    "AllowedMethods": ["GET", "POST", "PUT"],
    "AllowedHeaders": ["Content-Type", "Content-Md5", "Content-Disposition"],
    "ExposeHeaders": ["Origin", "Content-Type", "Content-Md5", "Content-Disposition"],
    "MaxAgeSeconds": 3600
  }
]

Then, we install the Active Storage JS code: yarn add @rails/activestorage or bin/importmap pin "@rails/activestorage".

We require it and call the start method in application.js or elsewhere:

import * as ActiveStorage from "@rails/activestorage";
ActiveStorage.start();

And finally, by passing direct_upload: true to the form.file_field helper we get direct uploads after users submit the form.

That's about it, now your application has direct uploads to Cloudflare R2 using Active Storage.

I hope you enjoyed the article and that it helped you build an awesome project with Rails. Have a good one!

Build your next rails app 10x faster with Avo

Avo dashboard showcasing data visualizations through area charts, scatterplot, bar chart, pie charts, custom cards, and others.

Find out how Avo can help you build admin experiences with Rails faster, easier and better.