Media to S3
WordPress plugin: migrate and serve uploads via S3-compatible storage
Install
The author publishes release zips, so WP-CLI can install straight from GitHub:
wp plugin install https://github.com/cxdy/media-to-s3/releases/download/v1.0.0/media-to-s3-1.0.0.zipReadme
Media to S3
logo generated by Google Gemini
Store WordPress media in S3-compatible object storage.
Same-host URLs or CDN · migrate from wp-admin or WP-CLI · new uploads follow
MIT OR GPL-2.0-or-later · WordPress 6.0+ · PHP 7.4+
Works with Amazon S3, Linode Object Storage, Cloudflare R2, MinIO, DigitalOcean Spaces, and any other S3-compatible API.
Documentation: https://cxdy.github.io/media-to-s3/
(source: Hugo site under docs/; guides for new sites and existing sites / migrate)
Requirements
- PHP 7.4+ (8.x preferred)
- WordPress 6.0+
- For zip installs: no Composer on the host (vendor is bundled in release zips)
- For development: Composer + PHPUnit
- WP-CLI optional (admin migrate works without it)
Install
WordPress.org / release zip
- Download
media-to-s3-x.y.z.zipfrom Releases (or WordPress.org when listed) - Upload under Plugins → Add New → Upload Plugin
- Activate
From source
cd wp-content/plugins
git clone https://github.com/cxdy/media-to-s3.git media-to-s3
cd media-to-s3
composer install --no-dev --optimize-autoloader
wp plugin activate media-to-s3
Quick start
- Media → Media to S3
- Pick a provider preset (or Custom), enter bucket + keys
- Click Test (PUT/GET/DELETE), then Save Changes
- Enable the plugin
- Greenfield: done — new uploads offload live
Existing library: open the Migrate tab (or use WP-CLI)
Step-by-step: new site · existing site
Prefer constants in wp-config.php (they override the options UI):
define('MEDIA_TO_S3_ENABLED', true);
define('MEDIA_TO_S3_ENDPOINT', 'https://us-ord-1.linodeobjects.com');
define('MEDIA_TO_S3_REGION', 'us-ord-1');
define('MEDIA_TO_S3_BUCKET', 'your-bucket');
define('MEDIA_TO_S3_ACCESS_KEY', getenv('MEDIA_TO_S3_ACCESS_KEY'));
define('MEDIA_TO_S3_SECRET_KEY', getenv('MEDIA_TO_S3_SECRET_KEY'));
define('MEDIA_TO_S3_DELETE_LOCAL', false); // default; keep locals until you choose otherwise
// Optional CDN:
// define('MEDIA_TO_S3_PUBLIC_BASE_URL', 'https://cdn.example.com/media');
Full variable list: Configuration.
Key layout
Local: wp-content/uploads/2020/03/foo.jpg
Key: 2020/03/foo.jpg (prefix empty)
URL: https://site/wp-content/uploads/2020/03/foo.jpg
Optional MEDIA_TO_S3_PREFIX=media → key media/2020/03/foo.jpg (same-host URL unchanged unless public_base_url is set).
Same-host vs CDN
| Mode | Setting | Browser URL |
|---|---|---|
| Same-host (default) | public_base_url empty |
/wp-content/uploads/... on your domain; plugin streams from the bucket when the local file is missing |
| Public/CDN | MEDIA_TO_S3_PUBLIC_BASE_URL set |
Attachment URLs rewrite to that base |
Historic post HTML rewrite is optional, default off, and has a dry-run under Tools.
WP-CLI
wp s3 doctor
wp s3 migrate --dry-run
wp s3 migrate --resume
wp s3 migrate --path=2020/03 --resume
wp s3 migrate --engine=php --concurrency=3
wp s3 migrate --engine=s5cmd --numworkers=128
wp s3 verify --sample=100
Admin Migrate tab provides background jobs with progress UI (resume / pause / cancel) for shared hosting without WP-CLI.
Migration runbook: Existing site guide · Migration reference.
Media Library
- List column: Offloaded / Local only / Local + remote / Missing remote
- Attachment metabox: push to S3, delete local if remote OK
- Bulk actions: push to S3, delete local when remote OK
How this differs from S3 Media Sync
Automattic’s S3 Media Sync solves a related problem—keeping WordPress media on Amazon S3—especially in VIP-oriented setups. This plugin is a separate project with a different audience and defaults.
| S3 Media Sync (Automattic) | This plugin (Media to S3) | |
|---|---|---|
| Audience | VIP / Automattic-oriented installs and tooling | General WordPress: shared hosting, VPS, self-hosted |
| Storage | AWS S3–focused | Any S3-compatible API (Amazon S3, Cloudflare R2, MinIO, DigitalOcean Spaces, Linode Object Storage, custom endpoints) |
| Public URLs | Sync/upload to the bucket (VIP-style workflows) | Same-host URLs by default (/wp-content/uploads/...); optional CDN / public base URL rewrite |
| Serving when local is gone | Not the same product model | Streams/proxies from the bucket when the local file is missing (same-host mode) |
| Migrate existing media | WP-CLI–forward (wp s3-media …) |
Admin background job (progress, resume, pause, cancel) and WP-CLI (wp s3 migrate, optional s5cmd engine) |
| Delete local after upload | Part of a tight S3 sync workflow | Available, but defaults off for safety |
| Distribution | GitHub / environment-specific install | Release zips with vendor bundled (no Composer on the host); aimed at WordPress.org |
If you already run VIP infrastructure and S3 Media Sync, stay there. If you want S3-compatible offload, same-host URLs, and migrate from wp-admin without VIP assumptions, this plugin is aimed at you.
Documentation
Operator docs are a Hugo site (GitHub Pages): https://cxdy.github.io/media-to-s3/
| Doc | Audience |
|---|---|
| Install | Operators — zip / source install |
| Usage | Operators — Test/Save, offload, Media Library |
| New site (greenfield) | Operators — no migration |
| Existing site (migrate) | Operators — migrate + cutover |
| Configuration | Operators — constants, presets, secrets |
| Same-host vs CDN | Operators — URL modes |
| Local development | Contributors — Docker WordPress + MinIO |
| Architecture | Contributors |
# Preview docs locally
cd docs && hugo server
Development
composer install
composer test
Unit tests cover pure helpers (key mapping, settings precedence, URL rewrite, error classification, content dry-run) without a live WordPress install.
Local WordPress + MinIO (Docker)
Spin up WordPress, MySQL, and MinIO, seed dummy posts/media, and exercise migrate + live uploads:
composer install --no-dev --optimize-autoloader
./bin/local-up.sh
# Site: http://localhost:8080 (admin / admin)
# MinIO console: http://localhost:9001 (minioadmin / minioadmin)
docker compose --profile cli run --rm wpcli s3 doctor
docker compose --profile cli run --rm wpcli s3 migrate --engine=php --resume
docker compose --profile cli run --rm wpcli s3 verify
Full notes: Local development.
Build a release zip (bundles vendor, excludes tests, Docker, and the Hugo docs site). Output is versioned from the plugin header (Version:) as dist/media-to-s3-1.0.0.zip; the folder inside the zip stays media-to-s3/ for WordPress installs:
bash bin/build-release-zip.sh
# optional override: VERSION=1.0.1 bash bin/build-release-zip.sh
Privacy
This plugin does not phone home, collect analytics, or call author-operated servers.
License
Dual-licensed: MIT OR GPL-2.0-or-later — see LICENSE.
Read the full README on GitHub →
Releases
| Tag | Published | Asset | Downloads |
|---|---|---|---|
| v1.0.0 | Aug 6, 2026 | media-to-s3-1.0.0.zip | 0 |