Local development
Docker Compose WordPress + MinIO for plugin development.
This stack runs WordPress, MySQL, and MinIO (S3-compatible) so you can exercise:
- Bulk migrate of existing uploads (
wp s3 migrateor Media → WP to S3 → Migrate) - Live offload of new Media Library uploads
- Same-host serve when a local file is deleted but the object exists in MinIO
The plugin is bind-mounted from the repo root into wp-content/plugins/wp-to-s3, so host edits apply immediately.
Prerequisites
- Docker + Docker Compose v2
- Composer (for
vendor/on the host; not required inside WordPress)
Quick start
# From the repo root
composer install --no-dev --optimize-autoloader # once (or when deps change)
./bin/local-up.sh
Or manually:
docker compose up -d
docker compose --profile seed run --rm seed
Open:
| URL | Purpose |
|---|---|
| http://localhost:8080 | WordPress |
| http://localhost:8080/wp-admin | Admin (admin / admin) |
| http://localhost:9000 | MinIO S3 API |
| http://localhost:9001 | MinIO console (minioadmin / minioadmin) |
Seed creates 12 dummy images and 6 posts (override with env — see below).
WP-CLI helpers
# Doctor / connectivity to MinIO
docker compose --profile cli run --rm wpcli s3 doctor
# Dry-run migrate
docker compose --profile cli run --rm wpcli s3 migrate --engine=php --dry-run
# Migrate remaining locals
docker compose --profile cli run --rm wpcli s3 migrate --engine=php --resume
# Verify local vs remote sizes
docker compose --profile cli run --rm wpcli s3 verify
# Arbitrary WP-CLI
docker compose --profile cli run --rm wpcli plugin list
Suggested test loop
- Seed already imported media; many files may already be on MinIO (live offload during
media import). - Run
wp s3 migrate --engine=php --resumeto catch anything still local-only. - In wp-admin, upload a new image → confirm it appears under the MinIO prefix (
local-dev/by default). - Delete a local file under
uploads/inside the WordPress volume and request the same-host URL — response should includeX-WP-To-S3: 1.
# Example: stream check (run on host)
FILE=$(docker compose --profile cli run --rm wpcli eval 'echo get_attached_file(4);')
# FILE is a path inside the container volume; use docker compose exec wordpress ...
Simpler manual check: after migrate, remove one file via:
docker compose exec wordpress rm -f /var/www/html/wp-content/uploads/202*/**/*.jpg # careful: pick one path
curl -sI http://localhost:8080/wp-content/uploads/... | grep -i x-wp-to-s3
Configuration
Seed writes these constants into wp-config.php (MinIO defaults):
| Constant | Default |
|---|---|
WP_TO_S3_ENABLED | true |
WP_TO_S3_ENDPOINT | http://minio:9000 |
WP_TO_S3_REGION | us-east-1 |
WP_TO_S3_BUCKET | wp-to-s3 |
WP_TO_S3_ACCESS_KEY | minioadmin |
WP_TO_S3_SECRET_KEY | minioadmin |
WP_TO_S3_PATH_STYLE | true |
WP_TO_S3_PREFIX | local-dev |
WP_TO_S3_DELETE_LOCAL | false |
Environment overrides
Create a root .env (gitignored) for Compose variable substitution:
WP_PORT=8080
WP_URL=http://localhost:8080
WP_ADMIN_USER=admin
WP_ADMIN_PASSWORD=admin
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
MINIO_BUCKET=wp-to-s3
WP_TO_S3_PREFIX=local-dev
SEED_IMAGE_COUNT=12
SEED_POST_COUNT=6
Re-seed content:
SEED_FORCE=1 docker compose --profile seed run --rm seed
Point at a real cloud bucket
After seed (or instead of MinIO), override constants:
docker compose --profile cli run --rm wpcli config set WP_TO_S3_ENDPOINT 'https://us-ord-1.linodeobjects.com'
docker compose --profile cli run --rm wpcli config set WP_TO_S3_REGION 'us-ord-1'
docker compose --profile cli run --rm wpcli config set WP_TO_S3_BUCKET 'your-bucket'
docker compose --profile cli run --rm wpcli config set WP_TO_S3_ACCESS_KEY '…'
docker compose --profile cli run --rm wpcli config set WP_TO_S3_SECRET_KEY '…'
docker compose --profile cli run --rm wpcli config set WP_TO_S3_PATH_STYLE false --raw
docker compose --profile cli run --rm wpcli s3 doctor
Reset
docker compose down -v # destroy DB + uploads + MinIO data
./bin/local-up.sh
Testing a release zip install
By default the repo is bind-mounted to wp-content/plugins/wp-to-s3. WordPress cannot replace that directory, so Plugins → Upload Plugin fails with:
The destination directory already exists and could not be removed.
That is expected. For day-to-day local work you do not need the zip — seed already activates the bind-mounted plugin.
To exercise a real zip install:
# Build + install via WP-CLI (drops bind-mount first)
./bin/local-zip-install.sh
# Or only prepare, then upload in wp-admin:
./bin/local-zip-install.sh --prepare-only
# → http://localhost:8080/wp-admin/plugin-install.php?tab=upload
# When done, restore live source mount:
docker compose up -d wordpress
docker-compose.nobind.yml overrides the wordpress/seed/wpcli volumes so the plugin path is a normal directory on the wp_data volume.
Notes
- Multisite is not supported by the plugin; this stack is single-site only.
miniovsminio-init:miniois the long-running S3 API (ports 9000/9001).minio-initis a one-shot that creates the bucket and exits 0 — “MinIO bucket ready” then stop is success, not a crash. It only runs under theseedprofile (docker compose --profile seed run --rm minio-init).- If seed fails with “vendor/ missing”, run
composer install --no-devon the host and re-run seed. - PHP
upload_max_filesize/post_max_sizeare set to 15M viadocker/php-uploads.iniso release zips fit under Plugins → Upload (only after dropping the bind-mount as above).