Local development

Docker Compose WordPress + MinIO for plugin development.

This stack runs WordPress, MySQL, and MinIO (S3-compatible) so you can exercise:

  1. Bulk migrate of existing uploads (wp s3 migrate or Media → WP to S3 → Migrate)
  2. Live offload of new Media Library uploads
  3. 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

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:

URLPurpose
http://localhost:8080WordPress
http://localhost:8080/wp-adminAdmin (admin / admin)
http://localhost:9000MinIO S3 API
http://localhost:9001MinIO 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

  1. Seed already imported media; many files may already be on MinIO (live offload during media import).
  2. Run wp s3 migrate --engine=php --resume to catch anything still local-only.
  3. In wp-admin, upload a new image → confirm it appears under the MinIO prefix (local-dev/ by default).
  4. Delete a local file under uploads/ inside the WordPress volume and request the same-host URL — response should include X-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):

ConstantDefault
WP_TO_S3_ENABLEDtrue
WP_TO_S3_ENDPOINThttp://minio:9000
WP_TO_S3_REGIONus-east-1
WP_TO_S3_BUCKETwp-to-s3
WP_TO_S3_ACCESS_KEYminioadmin
WP_TO_S3_SECRET_KEYminioadmin
WP_TO_S3_PATH_STYLEtrue
WP_TO_S3_PREFIXlocal-dev
WP_TO_S3_DELETE_LOCALfalse

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