# Object storage migration RustFS is the normal S3-compatible store. MinIO and `storage_migrate` are gated behind the `storage-migration` Compose profile and are not started by a normal `docker compose up -d`. On the first production release with a legacy MinIO container, the deployment workflow pauses app/scheduler writers, starts both stores, copies every bucket and its current objects (including `mineseeker` media and LiipImagine cache thumbnails), verifies each source/target key-and-size inventory, then starts the app on RustFS. It also carries over bucket policies, while enforcing the existing public-read policy for `mineseeker/media/*` and `mineseeker/cache/*`. Versioned or suspended-versioning buckets fail safely instead of silently losing historical versions/delete markers. Only after successful migration and app startup does it remove the MinIO and migration containers. It never removes the named `minio_data` volume; keep that volume as the rollback/backup until RustFS has been verified and a separate retirement decision is made. A missing source bucket or inventory mismatch fails the deployment rather than recording an empty/partial migration. RustFS retains the public host ports `9010` (S3 API) and `9011` (console); the legacy MinIO temporarily uses `9012` and `9013`. Confirm the public storage hostname configured by `MINIO_PUBLIC_URL` (for example, `d.aws...`) proxies to the RustFS API port before serving the new app. The legacy MinIO uses the local image named by `MINIO_IMAGE` (defaulting to the previously configured release tag) and does not try to pull it. If production has another local image tag, set `MINIO_IMAGE` to that exact reference before deployment. For future releases, the workflow sees no MinIO container and deploys only the RustFS stack. Review the release logs and `docker compose logs rustfs` after rollout; never use `docker compose down -v` as part of migration/retirement. ## Testing a backup in isolation `bin/restore-minio.sh` erases the MinIO volume belonging to the selected Compose project before extracting the archive. Test with a separate project name and `APP_NAME`; Compose then uses separate containers, network, and named volumes. Use alternate published ports so the test services can run beside the regular development stack: ```bash export COMPOSE_PROJECT_NAME=mine-minio-test export APP_NAME=mine-minio-test export MINIO_PUBLISHED_API_PORT=19102 export MINIO_PUBLISHED_CONSOLE_PORT=19103 export RUSTFS_PUBLISHED_API_PORT=19100 export RUSTFS_PUBLISHED_CONSOLE_PORT=19101 export APP_PUBLISHED_PORT=19180 export POSTGRES_PUBLISHED_PORT=15452 export MAILHOG_SMTP_PUBLISHED_PORT=19125 export MAILHOG_UI_PUBLISHED_PORT=19126 docker compose --profile storage-migration up -d minio rustfs bin/restore-minio.sh bazmeg/mineseeker-minio.2026-10-24.162934.tar.gz docker compose --profile storage-migration up --build --abort-on-container-exit --exit-code-from storage_migrate storage_migrate docker compose logs storage_migrate ``` The restore confirmation names the test MinIO volume; verify it says `mine-minio-test_minio_data` before typing `yes`. The migration's state volume is also isolated. This starts only MinIO and RustFS before the explicit migration—not the app, scheduler, or database. To start the test app only after the migration succeeds, restore its PostgreSQL backup, then run `MINIO_PUBLIC_URL=http://localhost:19100 docker compose up -d app scheduler` so browser-generated cache URLs reach the isolated RustFS. `docker compose down` does not delete the test volumes; avoid `down -v` until you intentionally want to discard the test copy.