Skip to main content

Cloudflare R2 Setup Guide

This guide walks you through setting up Cloudflare R2 as your storage provider. For a comparison between MinIO and R2, see Storage Configuration.


Prerequisites

  • A Cloudflare account (free)
  • A domain managed by Cloudflare (for custom domain setup)
  • A credit card on file (required by R2, but free tier covers most usage)

Step 1: Find Your Account ID

  1. Log in to Cloudflare Dashboard
  2. Your Account ID is in the right sidebar of the overview page, or in the URL: https://dash.cloudflare.com/<ACCOUNT_ID>
  3. Save this — you need it as R2__AccountId

Step 2: Create Buckets

  1. Go to R2 Object Storage in the left sidebar
  2. If first time, accept terms and add a payment method
  3. Create these 5 buckets (click Create bucket for each):
BucketPurposePublic Access?
publicAvatars, post images/videos, processed mediaYes
tempTemporary uploads awaiting processingYes
filesBlog file downloads, attachmentsYes
supportSupport/ticket attachmentsYes
backupsDatabase & system backupsNo

Region: Choose the closest to your users. "Automatic" or "Asia-Pacific (APAC)" for Southeast Asia.

Auto-creation

The backend auto-creates buckets if they don't exist. However, creating them manually first is recommended because public access and custom domains can only be configured in the Dashboard.


Step 3: Create an R2 API Token

  1. Go to R2 Object Storage main page
  2. Click Manage R2 API Tokens (top right)
  3. Click Create API Token
  4. Configure:
    • Token name: e.g., BellamyBook Production
    • Permissions: Object Read & Write
    • Buckets: Apply to all buckets (or select the 5 above)
  5. Click Create API Token
  6. Copy both values immediately (the secret is only shown once!):
    • Access Key IDR2__AccessKey
    • Secret Access KeyR2__SecretKey

Step 4: Set Up Cloudflare Worker

A Cloudflare Worker routes all buckets under one custom domain and enforces access control (referer restrictions).

4a. Create the Worker

  1. Go to Workers & Pages in the sidebar
  2. Click CreateCreate Worker
  3. Name it: r2-router
  4. Click Deploy, then click Edit code
  5. Replace the code with the Worker from the R2 Setup Guide (Step 5a)
  6. Click Save and Deploy

The Worker handles:

  • Routing/public/*, /temp/*, /files/*, /support/*, /backups/* to the correct bucket
  • Referer restrictionspublic, temp, support buckets only allow requests from your domains
  • Open pathspublic/blogs/featured/* is open to everyone (SEO)
  • Private accessbackups always returns 403
  • Video streaming — Full range request support (206 Partial Content)
  • CORS — Cross-origin headers for frontend media loading

4b. Bind R2 Buckets

  1. Go to Workers & Pages → click r2-router
  2. SettingsBindingsAddR2 bucket for each:
Variable nameR2 bucket
PUBLIC_BUCKETpublic
TEMP_BUCKETtemp
FILES_BUCKETfiles
SUPPORT_BUCKETsupport
BACKUPS_BUCKETbackups
  1. Click Save and Deploy

4c. Add Custom Domain

  1. On the r2-router worker → SettingsDomains & Routes
  2. Click AddCustom domain
  3. Enter: r2.yourdomain.com
  4. Cloudflare auto-creates the DNS record

4d. Disable r2.dev Subdomain

For production security, disable the r2.dev public URLs (they bypass the Worker):

For each bucket (public, temp, files, support):

  1. Click the bucket → SettingsPublic access
  2. R2.dev subdomain → click Disallow Access

Step 5: Configure Your App

Backend

Storage__Provider=R2
R2__AccountId=your_account_id
R2__AccessKey=your_access_key
R2__SecretKey=your_secret_key
R2__PublicUrl=https://r2.yourdomain.com

Frontend & Admin

VITE_STORAGE_PUBLIC_URL=https://r2.yourdomain.com

Test

curl -I https://r2.yourdomain.com/public/test.jpg
# Should return 200 (or 403 if referer check blocks it — expected for direct access)

Backup with R2

Backup is not affected by switching from MinIO to R2. The app uses a single object-storage client (IMinioService), which is either MinIO or R2 based on Storage:Provider. When you set Storage:Provider=R2, backup automatically uses R2 for storing backup archives.

How it works

  • Backup storage is chosen by Backup:Storage:Type. For cloud storage you set it to MinIO (the name is historical; it uses the same S3-compatible client).
  • The backup service uses the same registered storage client as media (public/temp/files). So with R2, backups go to your R2 backups bucket using your existing R2 credentials — no separate backup endpoint or keys.
  • The Backup:Storage:MinIO section in config is only used for the bucket name (Bucket, default backups). Endpoint/AccessKey/SecretKey under Backup:Storage:MinIO are not used for the backup client when the app runs with R2; the app uses R2:AccountId, R2:AccessKey, R2:SecretKey from the main storage config.

Production config

  1. Create the backups bucket in the R2 dashboard (see Step 2: Create Buckets). Keep it private (no public access).
  2. Backend config — same as media; no extra backup-specific R2 config:
    Storage__Provider=R2
    R2__AccountId=...
    R2__AccessKey=...
    R2__SecretKey=...
    R2__PublicUrl=https://r2.yourdomain.com
  3. Backup section — use cloud storage and the backups bucket name:
    "Backup": {
    "Enabled": true,
    "Storage": {
    "Type": "MinIO",
    "MinIO": { "Bucket": "backups" }
    }
    }
    Or via env: Backup__Storage__Type=MinIO, Backup__Storage__MinIO__Bucket=backups.

Effect of switching MinIO ↔ R2

ScenarioBackup destination
Storage:Provider=MinIOMinIO backups bucket (Minio endpoint/keys)
Storage:Provider=R2R2 backups bucket (R2 credentials)

If you migrate from MinIO to R2, copy existing backup archives to R2 (e.g. rclone sync minio:backups r2:backups) so you can still list/restore them. New backups will then go to R2 only.


Bucket Policies

R2 doesn't support S3 bucket policies. Access control is handled by the Worker:

BucketMinIO Policy FileWorker Behavior
publicpublic-bucket-policy.jsonReferer check + blogs/featured/* open
temptemp-bucket-policy.jsonReferer check
supportsupport-bucket-policy.jsonReferer check
files(none — fully public)No restrictions
backups(none — private)Always returns 403

To update allowed domains, edit the allowedReferers array in the Worker code and redeploy. Facebook and other networks often fetch preview images without a Referer header; the Worker must also allow known link-preview crawler User-Agents (see documents/R2_CLOUDFLARE_SETUP_GUIDE.md).


Migration from MinIO

If you have existing data in MinIO, use rclone to migrate:

# Configure both remotes
rclone config # Add 'minio' (S3/Minio) and 'r2' (S3/Cloudflare)

# Sync each bucket
rclone sync minio:public r2:public --progress
rclone sync minio:temp r2:temp --progress
rclone sync minio:files r2:files --progress
rclone sync minio:support r2:support --progress
rclone sync minio:backups r2:backups --progress

Pricing

ResourceFree TierPaid
Storage10 GB / month$0.015 / GB
Writes (Class A)1M / month$4.50 / million
Reads (Class B)10M / month$0.36 / million
EgressAlways freeAlways free
Worker requests100K / day$5/month for 10M

Next Steps