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
- Log in to Cloudflare Dashboard
- Your Account ID is in the right sidebar of the overview page, or in the URL:
https://dash.cloudflare.com/<ACCOUNT_ID> - Save this — you need it as
R2__AccountId
Step 2: Create Buckets
- Go to R2 Object Storage in the left sidebar
- If first time, accept terms and add a payment method
- Create these 5 buckets (click Create bucket for each):
| Bucket | Purpose | Public Access? |
|---|---|---|
public | Avatars, post images/videos, processed media | Yes |
temp | Temporary uploads awaiting processing | Yes |
files | Blog file downloads, attachments | Yes |
support | Support/ticket attachments | Yes |
backups | Database & system backups | No |
Region: Choose the closest to your users. "Automatic" or "Asia-Pacific (APAC)" for Southeast Asia.
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
- Go to R2 Object Storage main page
- Click Manage R2 API Tokens (top right)
- Click Create API Token
- Configure:
- Token name: e.g.,
BellamyBook Production - Permissions: Object Read & Write
- Buckets: Apply to all buckets (or select the 5 above)
- Token name: e.g.,
- Click Create API Token
- Copy both values immediately (the secret is only shown once!):
- Access Key ID →
R2__AccessKey - Secret Access Key →
R2__SecretKey
- Access Key ID →
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
- Go to Workers & Pages in the sidebar
- Click Create → Create Worker
- Name it:
r2-router - Click Deploy, then click Edit code
- Replace the code with the Worker from the R2 Setup Guide (Step 5a)
- Click Save and Deploy
The Worker handles:
- Routing —
/public/*,/temp/*,/files/*,/support/*,/backups/*to the correct bucket - Referer restrictions —
public,temp,supportbuckets only allow requests from your domains - Open paths —
public/blogs/featured/*is open to everyone (SEO) - Private access —
backupsalways returns 403 - Video streaming — Full range request support (206 Partial Content)
- CORS — Cross-origin headers for frontend media loading
4b. Bind R2 Buckets
- Go to Workers & Pages → click
r2-router - Settings → Bindings → Add → R2 bucket for each:
| Variable name | R2 bucket |
|---|---|
PUBLIC_BUCKET | public |
TEMP_BUCKET | temp |
FILES_BUCKET | files |
SUPPORT_BUCKET | support |
BACKUPS_BUCKET | backups |
- Click Save and Deploy
4c. Add Custom Domain
- On the
r2-routerworker → Settings → Domains & Routes - Click Add → Custom domain
- Enter:
r2.yourdomain.com - 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):
- Click the bucket → Settings → Public access
- 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 toMinIO(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
backupsbucket using your existing R2 credentials — no separate backup endpoint or keys. - The
Backup:Storage:MinIOsection in config is only used for the bucket name (Bucket, defaultbackups). Endpoint/AccessKey/SecretKey underBackup:Storage:MinIOare not used for the backup client when the app runs with R2; the app usesR2:AccountId,R2:AccessKey,R2:SecretKeyfrom the main storage config.
Production config
- Create the
backupsbucket in the R2 dashboard (see Step 2: Create Buckets). Keep it private (no public access). - Backend config — same as media; no extra backup-specific R2 config:
Storage__Provider=R2R2__AccountId=...R2__AccessKey=...R2__SecretKey=...R2__PublicUrl=https://r2.yourdomain.com
- Backup section — use cloud storage and the backups bucket name:
Or via env:"Backup": {"Enabled": true,"Storage": {"Type": "MinIO","MinIO": { "Bucket": "backups" }}}
Backup__Storage__Type=MinIO,Backup__Storage__MinIO__Bucket=backups.
Effect of switching MinIO ↔ R2
| Scenario | Backup destination |
|---|---|
Storage:Provider=MinIO | MinIO backups bucket (Minio endpoint/keys) |
Storage:Provider=R2 | R2 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:
| Bucket | MinIO Policy File | Worker Behavior |
|---|---|---|
public | public-bucket-policy.json | Referer check + blogs/featured/* open |
temp | temp-bucket-policy.json | Referer check |
support | support-bucket-policy.json | Referer 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
| Resource | Free Tier | Paid |
|---|---|---|
| Storage | 10 GB / month | $0.015 / GB |
| Writes (Class A) | 1M / month | $4.50 / million |
| Reads (Class B) | 10M / month | $0.36 / million |
| Egress | Always free | Always free |
| Worker requests | 100K / day | $5/month for 10M |
Next Steps
- Storage Configuration — Compare MinIO vs R2
- Environment Configuration
- Backups