Due to increasing dissatisfaction with domestic cloud vendors and to improve blog service performance, I’m migrating the blog’s static resources from Tencent Cloud to Cloudflare R2. The steps are quite tedious, including rclone configuration, Worker deployment, and custom domain binding. However, with AI assistance, the entire migration process took about 1 hour, and 0.5GB of static resources were switched over with zero downtime. Here’s a complete record of the steps for everyone’s reference.
Background and Goals
The main purposes of migrating static resources from Tencent Cloud to Cloudflare R2 are:
- Cost Reduction: Cloudflare R2 has completely free egress traffic. In the future, I plan to abandon services that rely on domestic clouds, as they are full of rogue practices from login to use.
- Performance Improvement: Utilize Cloudflare’s global CDN acceleration to optimize blog access speed and SEO.
Final Architecture
Server local directory
/var/www/blog/static-assets
↓ (rclone sync)
Cloudflare R2 (private bucket)
↓ (Worker)
static.1991421.cn (custom domain)
Architecture Features:
- Blog URLs remain unchanged
- R2 Bucket remains private
- Access controlled via Worker
- Zero egress traffic costs
1. Install rclone
Install rclone on the server:
curl https://rclone.org/install.sh | sudo bash
2. Configure rclone to Connect to R2
1. Start Configuration Wizard
rclone config
2. Create a New Remote
Configure according to the following steps:
- Select
nto create a new remote - Name:
cf-r2 - Storage type:
s3(S3 compatible object storage) - Provider:
47(Any other S3 compatible provider)
Why not select Cloudflare R2 option? R2 is not in the built-in vendor list, so we must use the generic S3 compatible configuration.
3. Fill in Authentication Information
After creating an API token in the Cloudflare R2 console, you will see three fields. Here’s the corresponding relationship:
| Cloudflare Console Field | rclone Config Item | Required? |
|---|---|---|
| Access Key ID | access_key_id | ✅ Required |
| Secret Access Key | secret_access_key | ✅ Required |
| Token Value | - | ❌ Ignore |
Enter in rclone:
access_key_id> [Your Access Key ID]
secret_access_key> [Your Secret Access Key]
4. Configure Connection Parameters
Region (leave empty):
region> [Press Enter directly]
R2 doesn’t have a region concept, leaving it empty is the official recommended approach.
Endpoint (required):
endpoint> https://<AccountID>.r2.cloudflarestorage.com
Example: https://xxxxx.r2.cloudflarestorage.com
You can find this address on the Bucket details page in the R2 console.
Location Constraint (leave empty):
location_constraint> [Press Enter directly]
ACL (empty = private):
acl> [Press Enter directly]
R2 doesn’t use S3 ACLs; permissions are managed by Workers.
Advanced config (not needed):
Edit advanced config? n
5. Confirm Configuration
y) Yes this is OK
Configuration complete!
3. Perform Data Migration
Initial Migration (Recommended: copy)
rclone copy /var/www/blog/static-assets cf-r2:static-assets \
--progress \
--transfers=16 \
--checkers=16
The copy command only copies files and doesn’t delete files on the destination, making it safer.
Daily Synchronization (Use sync)
rclone sync /var/www/blog/static-assets cf-r2:static-assets \
--progress \
--transfers=16 \
--checkers=16
sync makes the destination exactly match the source and will delete excess files on the destination.
Parameter Explanations:
--progress: Show transfer progress--transfers=16: Transfer 16 files concurrently--checkers=16: Check 16 files concurrently
4. Configure Cloudflare R2 Bucket
Create an R2 Bucket in the Cloudflare console:
- Bucket Name:
static-assets - Storage Type: Standard
- Region: Auto
- Public Access: ❌ Disabled (keep private)
Important: Do not enable public access; we will control access through Workers.
5. Deploy Cloudflare Worker
1. Create a Worker
In the Cloudflare console:
- Go to Workers & Pages
- Click Create Application
- Select Create Worker
- Select Start from Hello World
2. Write Worker Code
Paste the following code into the Worker editor:
export default {
async fetch(request, env) {
const url = new URL(request.url);
const key = url.pathname.slice(1);
if (!key) {
return new Response('Not Found', { status: 404 });
}
const object = await env.STATIC_ASSETS.get(key);
if (!object) {
return new Response('Not Found', { status: 404 });
}
return new Response(object.body, {
headers: {
'Content-Type':
object.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}
};
Code Explanation:
- Extract file key from URL path
- Get object from R2
- Set correct Content-Type
- Add long-term cache headers (1 year)
3. Bind R2 Bucket
This is the most critical step and is easily overlooked!
In Worker settings:
- Go to Settings > Variables and Secrets
- Select R2 Bucket Bindings (not environment variables!)
- Add binding:
- Variable Name:
STATIC_ASSETS - R2 Bucket: Select
static-assets
- Variable Name:
Note: Must use R2 Bucket binding; do not use environment variables or plain text.

4. Test Worker
After deployment, test using the workers.dev domain:
curl -I https://static-assets.your-account.workers.dev/2018-06-06-Validate.jpg
If it returns the image correctly, the Worker to R2 connection is established.
6. Bind Custom Domain
1. Add Custom Domain
In Worker settings:
- Go to Triggers
- Click Add Custom Domain
- Enter domain:
static.1991421.cn
If the domain is already managed by Cloudflare:
- DNS records will be created automatically
- HTTPS will be enabled automatically
- Usually takes effect within 1 minute
2. Verify Domain
curl -I https://static.1991421.cn/2018-06-06-Validate.jpg
Check response headers:
server: cloudflare
cache-control: public, max-age=31536000, immutable
These indicate that it’s officially online!
7. Update Blog References
If you used a new domain, you need to update the static resource URLs in your blog. In my case, they remain unchanged, so this step isn’t necessary. If you need to update, you can use batch replacement:
Old: https://old-domain.com/image.jpg
New: https://static.1991421.cn/image.jpg
You can use a batch replacement tool to complete this.
Summary
This migration achieved:
✅ Cost Optimization: R2 egress traffic is $0 ✅ Performance Improvement: Cloudflare global CDN acceleration ✅ Architecture Optimization: Standard practice of Private Bucket + Worker ✅ Zero Downtime Migration: URLs remain unchanged, users are unaware
The entire migration process took only 1 hour, and all 0.5GB of static resources were migrated. This is the Cloudflare R2 + Worker approach.


