Running WordPress media libraries directly on VPS local disks creates persistent operational bottlenecks: server disk space fills up rapidly, automated backup archives balloon to gigabytes, and single-server image delivery degrades latency for international visitors.
Cloudflare R2 provides an S3-compatible cloud object storage service featuring zero egress bandwidth fees and standard storage pricing of $0.015 per GB-month. However, legacy WordPress offload plugins typically bundle the official Amazon AWS PHP SDK—introducing over 35MB of third-party vendor code and consuming 15MB to 30MB of PHP runtime RAM per request.
This guide walks through configuring iz-r2-media, a lightweight, zero-SDK media offloader built with a native pure-PHP AWS Signature Version 4 (SigV4) cURL transport (under 60KB total code, under 1MB RAM overhead).
Technical Architecture Overview
The iz-r2-media engine replaces the standard WordPress media upload and retrieval pipeline with non-blocking, cloud-native storage primitives:
┌────────────────────────────────────────────────────────────────────────┐
│ iz-r2-media Storage Pipeline │
│ │
│ 1. Media Upload (WP Admin / REST API) │
│ │ │
│ ├── wp_generate_attachment_metadata │
│ └── Intercepted by UploadHandler │
│ │ │
│ ├── Pure PHP AWS SigV4 Signer (HMAC-SHA256, Zero-SDK) │
│ ├── Direct HTTP PUT stream via cURL to Cloudflare R2 │
│ └── Attachment PostMeta marked: _iz_r2_offloaded = 1 │
│ │
│ 2. Frontend Delivery & URL Rewriting │
│ │ │
│ ├── wp_get_attachment_url & wp_calculate_image_srcset │
│ └── Rewritten to Custom CDN Domain (https://media.example.com/..) │
│ │
│ 3. Administrative Safety (keep_local = true by default) │
│ │ │
│ ├── Local copy preserved on VPS disk │
│ └── Zero 404 errors if plugin is temporarily deactivated │
└────────────────────────────────────────────────────────────────────────┘
Step 1: Obtain Cloudflare R2 Credentials & Parameters
To configure iz-r2-media, you need 5 configuration values from your Cloudflare account:
- Account ID
- Bucket Name
- Access Key ID
- Secret Access Key
- Custom CDN Domain
Here is the exact procedure to extract each value from the Cloudflare Dashboard.
1. Find Your Cloudflare Account ID
There are two fast methods to locate your Account ID:
- From the URL: Look at your browser address bar when logged into Cloudflare. The 32-character hexadecimal string immediately following
dash.cloudflare.com/is your Account ID:https://dash.cloudflare.com/c3f4e289a0b123456789abcdef012345/r2/overview └───────────────────────────────┘ Your Account ID - From the R2 Overview Sidebar: Navigate to R2 in the left menu. In the right-hand sidebar under Account details, click the copy icon next to Account ID.
2. Create Your R2 Storage Bucket (Bucket Name)
- In Cloudflare Dashboard, click R2 in the left navigation menu.
- Click the blue Create bucket button.
- Enter your Bucket name (e.g.,
prod-wp-media).- Use lowercase alphanumeric characters and hyphens only.
- This exact name is your Bucket Name parameter.
- Choose a Location hint (e.g.,
APACfor Asia-Pacific orWNAMfor Western North America) closest to your primary audience. - Click Create Bucket.
3. Generate Scoped API Tokens (Access Key ID & Secret Access Key)
To adhere to least-privilege security principles, generate an R2 API token restricted exclusively to your WordPress media bucket:
- From the R2 Overview page, click Manage R2 API Tokens in the top-right corner.
- Click Create API token.
- Configure the token permissions:
- Token name: Enter an identifier such as
WordPress iz-r2-media. - Permissions: Select Object Read & Write. (Do not select “Admin Read & Write” to avoid granting bucket deletion privileges).
- Specify bucket(s): Choose Apply to specific buckets only, then select the bucket created above (
prod-wp-media). - Client IP Address Filtering: Optional. Leave blank for standard web servers.
- TTL / Expiration: Leave unset for permanent production use, or set per your compliance schedule.
- Token name: Enter an identifier such as
- Click Create API Token at the bottom of the page.
- Save Your Credentials Immediately: Cloudflare will display a credentials summary screen:
- Access Key ID: Copy the 32-character hexadecimal string.
- Secret Access Key: Copy the 64-character hash string.
- Important Security Note: The Secret Access Key is displayed only once. If you close or refresh this tab before copying, it cannot be recovered and you will need to generate a new token.
4. Connect a Custom CDN Domain (Custom Domain)
Connecting a custom subdomain (e.g., media.example.com) ensures all media files are served through Cloudflare edge caching under your own SSL-secured domain:
- In Cloudflare Dashboard, go to R2 and click into your bucket (
prod-wp-media). - Click the Settings tab.
- Scroll down to the Public access section.
- Under Custom Domains, click Connect Domain.
- Enter your desired subdomain:
media.example.com. (The root domainexample.commust be an active zone in your Cloudflare account). - Click Continue, review the DNS CNAME record that Cloudflare will automatically create, and click Connect Domain.
- Wait 30 to 60 seconds until the status shows Active.
- Your Custom Domain value to paste into WordPress is:
https://media.example.com(must include thehttps://prefix).
Cloudflare-to-WordPress Parameter Mapping Matrix
| Parameter in WordPress Plugin | Source in Cloudflare Dashboard | Example Value |
|---|---|---|
| Account ID | R2 Overview -> Right Sidebar -> Account details | c3f4e289a0b123456789abcdef012345 |
| Bucket Name | R2 -> Bucket Name created in Step 1.2 | prod-wp-media |
| Access Key ID | R2 API Tokens -> Create API Token screen | 7a19cb02e123456789abcdef01234567 |
| Secret Access Key | R2 API Tokens -> Generated token screen (shown once) | 99f01e44d123456789abcdef0123456789abcdef... |
| Custom Domain | R2 Bucket -> Settings -> Public access -> Custom Domains | https://media.example.com |
| Keep Local Files | WordPress Settings checkbox (keep_local) | Checked (Preserves local disk copies) |
Step 2: Install and Activate iz-r2-media
Download and install the plugin using your preferred administration method.
Method A: WordPress Admin Dashboard
- Download
iz-r2-media.zipfrom the official repository or release page. - In WordPress Admin, navigate to Plugins -> Add New -> Upload Plugin.
- Select
iz-r2-media.zipand click Install Now. - Click Activate Plugin.
Method B: WP-CLI
wp plugin install https://github.com/izhubs/iz-r2-media/releases/latest/download/iz-r2-media.zip --activate
Step 4: Configure Storage Parameters
The plugin supports two configuration layers: Web Admin Settings (wp_options) and Twelve-Factor Environment Constants.
Option A: Web Admin Settings UI
Navigate to Settings -> Cloudflare R2 Media in your WordPress dashboard:
| Field Label | Parameter Key | Description | Example Value |
|---|---|---|---|
| Account ID | account_id | Cloudflare Account ID | c3f4e2...89a |
| Access Key ID | access_key | Scoped R2 Access Key ID | 7a19c...b02 |
| Secret Access Key | secret_key | Scoped R2 Secret Access Key | 99f01e...44d |
| Bucket Name | bucket | Target R2 bucket name | prod-wp-media |
| Custom Domain | custom_domain | Connected Cloudflare CDN domain | https://media.example.com |
| Keep Local Files | keep_local | Retain local files on VPS disk | Checked (Default) |
Click Save Changes. The plugin executes a live SigV4 test ping against your R2 bucket and displays connection status.
Option B: 12-Factor Environment Configuration (Production Recommended)
For containerized setups (Docker, Roots Bedrock, izdeploy), define constants in wp-config.php or inject them through system environment variables. This prevents API keys from being stored in database dumps and locks the web UI against unauthorized modifications:
// wp-config.php
define( 'IZ_R2_ACCOUNT_ID', 'c3f4e289a0b123456789abcdef012345' );
define( 'IZ_R2_ACCESS_KEY', '7a19cb02e123456789abcdef01234567' );
define( 'IZ_R2_SECRET_KEY', '99f01e44d123456789abcdef0123456789abcdef0123456789abcdef01234567' );
define( 'IZ_R2_BUCKET', 'prod-wp-media' );
define( 'IZ_R2_CUSTOM_DOMAIN', 'https://media.example.com' );
define( 'IZ_R2_KEEP_LOCAL', true );
When constants are defined, all settings fields in the WordPress Admin UI display as read-only with an administrative badge: Locked: Controlled via wp-config.php.
Step 5: Migrate Existing Media Library
Existing media files uploaded prior to plugin installation require migration to Cloudflare R2.
Web Migration (Action Scheduler)
- In WordPress Admin, navigate to Settings -> Cloudflare R2 Media.
- Scroll to the Bulk Media Migration card.
- Click Start Background Migration.
- The migration engine queues tasks in batches of 25 items using Action Scheduler, preventing PHP execution timeouts. A progress bar updates in real time.
High-Concurrency WP-CLI Migration (Pro Edition)
For catalogs exceeding 10,000 files, the commercial Pro add-on (iz-r2-media-pro) provides socket-level multiplexed migration via cURL multi-handle pooling:
# Execute 8-stream parallel migration
wp iz-r2 sync --concurrency=8
# Migrate with 16 parallel sockets and unlink verified local files
wp iz-r2 sync --concurrency=16 --delete-local --batch-size=100
# Perform dry-run simulation
wp iz-r2 sync --dry-run
Step 6: Deactivation Lifecycle & Safe Rollback
Understanding plugin deactivation mechanics is essential for operational reliability:
1. New Uploads After Deactivation
When iz-r2-media is deactivated, WordPress unhooks the upload handlers. Newly uploaded media files immediately revert to standard core storage (wp-content/uploads/ on local VPS disk).
2. Previously Offloaded Media
- With Default Setting (
keep_local = true): Local file copies remain on your VPS disk. When the plugin is deactivated, WordPress URL rewriting filters detach. Media URLs automatically revert to your primary domain (https://example.com/wp-content/uploads/...). Assets continue serving directly from your web server without broken images or 404 errors. - With Disk Savings Setting (
keep_local = false): Local files were removed upon upload to save server disk. Deactivating the plugin causes WordPress core to query local paths that no longer exist, producing HTTP 404 responses. Prior to deactivating under this configuration, download your media assets from Cloudflare R2 back towp-content/uploads/.
Summary Specification
| Technical Metric | Benchmark |
|---|---|
| Plugin Package Footprint | 54 KB (Community) / 30 KB (Pro Add-on) |
| PHP Runtime Memory Overhead | Under 1.0 MB |
| External Dependencies | 0 (Native cURL + PHP OpenSSL extensions) |
| WordPress Compatibility | WordPress 6.0 to 6.7+ |
| PHP Runtime Compatibility | PHP 8.1, 8.2, 8.3, 8.4 |
| Supported Storage Endpoints | Cloudflare R2 (S3 API Standard) |
| License | GPLv2 or later |