Cloudflare R2 — Plan

Complexity: Low-Medium Integration: Cloudflare R2 (S3-compatible) for ActiveStorage file uploads in production


External Setup

  1. Log into Cloudflare Dashboard: https://dash.cloudflare.com
  2. R2 Object Storage → Activate (free tier: 10GB storage, 10M reads/mo)
  3. Create bucket: name your-app-production
  4. Note Account ID (32-char hex, visible in dashboard URL)
  5. Create R2 API Token: R2 → Manage R2 API Tokens → Create → "Object Read & Write" → note Access Key ID + Secret Access Key
  6. Set up public custom domain:
    • R2 → bucket → Settings → Public Access → Custom Domain
    • Add cdn.yourdomain.com
    • Domain must be on Cloudflare DNS — add CNAME record
    • Enable public access
  7. (Optional) Set CORS policy: R2 → Bucket → Settings → CORS → allow your app's origin
  8. (Optional) Set Cache Rule: Caching → Cache Rules → aggressive caching for cdn.yourdomain.com/*

Code Changes

  1. Add to Gemfile: gem "aws-sdk-s3", require: false
  2. bundle install
  3. Add credentials via bin/rails credentials:edit:
    cloudflare:
      r2_account_id: "your-cloudflare-account-id"
      r2_access_key_id: "your-r2-access-key-id"
      r2_secret_access_key: "your-r2-secret-access-key"
      r2_bucket: "your-bucket-name"
    
  4. Add to config/storage.yml:
    cloudflare:
      service: S3
      access_key_id: <%= Rails.application.credentials.dig(:cloudflare, :r2_access_key_id) %>
      secret_access_key: <%= Rails.application.credentials.dig(:cloudflare, :r2_secret_access_key) %>
      region: auto
      bucket: <%= Rails.application.credentials.dig(:cloudflare, :r2_bucket) %>
      endpoint: https://<%= Rails.application.credentials.dig(:cloudflare, :r2_account_id) %>.r2.cloudflarestorage.com
      force_path_style: true
      public: true
    
  5. Update config/environments/production.rb: config.active_storage.service = :cloudflare
  6. Update CSP in config/initializers/content_security_policy.rb: add cdn.yourdomain.com to img_src
  7. Dev stays on :local — no changes needed

Verification

  • Upload avatar/project logo in production → appears in R2 bucket (Cloudflare dashboard)
  • Images load from cdn.yourdomain.com in browser
  • Image variants (thumbnails) generate correctly