Tutorial

Migrate MinIO to Garage or RustFS without losing a current object

Self-hosting13 min read20 steps

The MinIO repository has been archived since 25 April 2026: your S3 storage now runs without upstream security fixes. Garage and RustFS are the two self-hostable targets that keep coming up, but copying the objects is only half the job. IAM policies, version history and presigned URLs do not follow. This guide inventories the source, copies with rclone, proves the result with counts and checks, then switches the application in three phases, with a minimal write cutoff and a rollback ready.

Contents· Why you should not leave MinIO where it is1/14
  1. 01Why you should not leave MinIO where it is
  2. 02What a bucket copy does not move by itself
  3. 03Prerequisites and capacity plan
  4. 04Step 1: inventory the source before copying
  5. 05Garage and RustFS: the gaps to know before choosing
  6. 06Versioning: what the copy loses, and what to do
  7. 07Step 2: prepare the target, Garage or RustFS
  8. 08Step 3: copy with rclone and prove the result
  9. 09ETag multipart and mc mirror: reading the gaps correctly
  10. 10Presigned URLs: why they fail at the switch
  11. 11Step 4: switch in three phases, without a hard cut
  12. 12Rolling back comes down to one variable
  13. 13Troubleshooting: the symptoms that keep coming back
  14. 14The official documentation

Why you should not leave MinIO where it is

The minio/minio repository was archived by its owner on 25 April 2026: it is read-only, its README states that the project is no longer maintained and that the binaries already published will not receive updates. In practice, the next flaw found in your S3 server will be fixed by nobody upstream. The mc client, also AGPLv3, was archived on 14 July 2026.

Your MinIO keeps running, which is what makes the subject treacherous: nothing breaks on the day of the archive. But object storage is exposed, it holds your media, backups or exports, and it now runs without security patches. Two self-hostable targets keep coming up in discussions: Garage, released under AGPLv3, and RustFS, released under the Apache 2.0 licence.

This article does not redo the installation of either: they have their own guides, Garage on a VPS and RustFS on a VPS. Here we start from a MinIO in production and answer one question: how to move the data, prove nothing is missing and switch the application over without broken URLs.

What a bucket copy does not move by itself

  • Current objects — the easy part: rclone or mc mirror copy them, with a check to run afterwards.
  • Older versions — the mc mirror documentation states that it only synchronises the current object, with no version information.
  • IAM users and policies — they live in MinIO, not in the buckets. Secrets cannot be exported: you generate new keys on the target.
  • Presigned URLs already handed out — the signature depends on the host, the region and the keys. Changing the endpoint invalidates them.
  • S3 compatibility gaps — ACLs, bucket policies, anonymous access, versioning: each target has its holes, to be read in its documentation rather than assumed.
  • Client configuration — endpoint, region, path-style addressing: everything your application, your restic backups or your CDN hard-code.

Prerequisites and capacity plan

You need administrator access to MinIO, a workstation or VPS with mc and rclone, and a target that is already installed and reachable over HTTPS (see the two guides above for the reverse proxy and the certificate). The capacity rule is simple: until you have validated the migration, you keep the source intact, so you pay for the disk twice.

Our editorial recommendation: plan on the target for the total size of the source data, plus a margin of about 30% for metadata and distribution differences. On the same VPS, that means having at least twice the current volume plus the margin; on two VPS, each carries its share. A single Garage node with replication_factor = 1 keeps only one copy of each object: it is not a backup, and the source still in place is your only safety net during the migration.

Step 1: inventory the source before copying

  1. Declare MinIO as an alias

    Create an alias src with credentials that can read every bucket:
    mc alias set src https://s3.your-domain.com ACCESS_KEY SECRET_KEY

  2. Count the objects and the size

    For each bucket, note the number of objects and the volume:
    mc du --recursive src/medias
    mc ls --recursive src/medias | wc -l
    Put these figures in a before / after table: it is your proof that the migration is complete.

  3. Count the versions

    If versioning is enabled, the version list is longer than the object list:
    mc ls --recursive --versions src/medias | wc -l
    The mc ls documentation specifies that --versions requires a service that supports versioning. A large gap between the two counts tells you how much history is at stake.

  4. Export the list of policies and users

    Keep a record of what you will have to recreate:
    mc admin policy list src
    mc admin user list src
    Secrets are not in this output and cannot be: you will regenerate them.

  5. Record the client configuration

    In every application that talks to MinIO, note the endpoint, the region, the addressing style and the name of the environment variable that carries them. The final switch will change only these values.

Garage and RustFS: the gaps to know before choosing

Scroll the table

TopicGarageRustFS
LicenceAGPLv3 (repository README)Apache 2.0 (repository README)
VersioningNot supported: `PutBucketVersioning` and `ListObjectVersions` are listed as missingListed as available in the README
Bucket policies and ACLsNeither: its own system, per key and per bucketIAM and bucket policies; ACLs are intentionally unsupported
Anonymous accessRefused, with a 403 responseEvaluated against the bucket policy
Client region`s3_region` from `garage.toml`, to be copied into the client`RUSTFS_REGION`, `us-east-1` by default, to be copied into the client
AddressingPath-style and vhost-style supportedPath-style by default, no DNS configuration
Presigned URLsSupportedSupported; host, scheme, port, region and path must match the signature
MinIO admin APINot applicable: you administer with the `garage` commandSeparate surface: S3 works without `mc admin` being guaranteed

Versioning: what the copy loses, and what to do

This point decides the choice of target. Garage does not support versioning: its S3 compatibility page says so plainly. If your buckets keep a history that the application reads or that you must keep for business reasons, Garage is not the destination. RustFS lists versioning as available, but copying with rclone sync or mc mirror will not recreate your history there.

Three routes, to choose bucket by bucket. Accept the loss: only the current version goes across, which suits cases where versioning was only a safety net against human error. Copy version by version: you list the identifiers with mc ls --versions, then replay the versions from oldest to newest to a target that supports versioning, after enabling it on the bucket; version dates will be those of the copy, not the originals. Or archive the history separately, in an archive bucket, and migrate only the current objects. In every case, test with an object that has several versions before launching the general copy.

Step 2: prepare the target, Garage or RustFS

  1. Garage: assign then apply the layout

    No bucket is usable until the layout is applied. Look up the node identifier, then assign it a zone and a capacity:
    garage status
    garage layout assign -z dc1 -c 1G <node_id>
    garage layout apply --version 1
    Adjust -c to the disk space actually allocated.

  2. Garage: create the key, the bucket and the permissions

    Permissions are granted per key and per bucket:
    garage key create app-key
    garage bucket create medias
    garage bucket allow --read --write --owner medias --key app-key
    The secret is displayed when the key is created: copy it straight into your secrets manager.

  3. Garage: copy the region into the client

    The [s3_api] section of garage.toml defines s3_region. Put exactly that value in your clients' configuration, and keep path-style addressing. A different region gives rejected signatures.

  4. RustFS: alias and bucket

    With mc configured against the target:
    mc alias set dst http://127.0.0.1:9000 ACCESS_KEY SECRET_KEY
    mc mb dst/medias
    The RustFS documentation recommends IAM users or service accounts rather than root keys for applications.

  5. RustFS: recreate users and policies

    Take the list exported in step 1 and recreate each user and each policy on the RustFS side, with a fresh key per application. Then align the region: RUSTFS_REGION is us-east-1 by default, and the client must use the same one.

Step 3: copy with rclone and prove the result

  1. Declare the two remotes

    Use the Minio provider for the source and Other for the target:
    rclone config create src s3 provider=Minio access_key_id=ACCESS_KEY secret_access_key=SECRET_KEY endpoint=https://s3.your-domain.com
    rclone config create dst s3 provider=Other access_key_id=ACCESS_KEY secret_access_key=SECRET_KEY endpoint=http://127.0.0.1:3900 region=garage
    region=garage assumes s3_region = "garage", the value in Garage's example: use yours if it differs. For RustFS, replace the endpoint with the target's and the region with the value of RUSTFS_REGION.

  2. Do a dry run

    rclone sync changes the destination so that it mirrors the source, deletions included. Never reverse it, and run the dry run first:
    rclone sync src:medias dst:medias --checksum --metadata --transfers 16 --dry-run

  3. Run the live copy

    MinIO stays in service during this first pass:
    rclone sync src:medias dst:medias --checksum --metadata --transfers 16 --progress
    --metadata asks rclone to preserve metadata: check on a test object that the content type arrived intact.

  4. Check the copy

    Check that everything in the source exists in the target:
    rclone check src:medias dst:medias --one-way --combined /tmp/check-medias.txt
    The file lists each path with a symbol: = identical, + missing from the destination, * different, ! error. Zero +, * or ! lines is the goal.

  5. Recount after the copy

    Replay mc du --recursive and the object count on the target, with the dst alias, and fill in the "after" column of your table. The two figures must be identical, except for gaps explained by versions.

ETag multipart and mc mirror: reading the gaps correctly

The ETag of a small object sent in a single request is its MD5, and rclone uses it as a checksum. For a multipart object, the rclone documentation points out that the ETag is no longer the MD5 of the data: it is the MD5 of the sequence of each part's MD5, followed by the number of parts. It therefore depends on the part size, which the client chose at upload time.

Your MinIO received the object with one split, rclone sends it to the target with another: the content is identical byte for byte, the ETag is not. A rclone check that reports these objects as different is not necessarily right. Replay it on them with --download, which downloads both sides and compares the data itself: slower, but unambiguous. The same caution applies to mc mirror: its documentation specifies that composite checksums can differ simply because the split into parts has changed.

If you prefer to stay within the mc ecosystem, the copy is written mc mirror --preserve src/medias dst/medias. --preserve keeps the source's attributes and bucket policy rules; the latter only makes sense if the target can read them, which rules out Garage. Without --overwrite, an object already on the target is not copied again: the error is logged and the copy continues. With --remove, target objects missing from the source are deleted. And mc mirror only synchronises the current object, without versions. Its comparison covers name, size, date and checksum, without reading the content. The mc repository is archived too: the tool works, but it will not move.

Presigned URLs: why they fail at the switch

A presigned URL contains a signature computed from the secret key, the host, the region and the path. The RustFS documentation says it: the host, scheme, port, region and request path must match those used to sign. Changing the endpoint or the key therefore invalidates every link already issued, without exception, and nothing redirects them.

The consequence is a matter of sequencing. Links your application generates on the fly regenerate themselves as soon as it is reconnected to the new target. Those you sent by e-mail or stored in a database will die at the switch. So shorten the lifetime of links a few days beforehand, so that few long-lived links are in circulation, and warn the teams who hand them out manually.

Step 4: switch in three phases, without a hard cut

  1. Initial live sync

    This is the copy from step 3, run while MinIO still serves traffic. It takes as long as it takes and bothers nobody. Note the end time.

  2. Freeze writes

    Put the application in read-only mode or in a short maintenance window, or enable dual writing if your code allows it. Reads can continue on MinIO while the final sync runs.

  3. Final sync with --update

    Transfer only what has changed since:
    rclone sync src:medias dst:medias --update --metadata --transfers 16 --progress
    rclone check src:medias dst:medias --one-way
    Recount the objects and the size, and compare with the table from step 1.

  4. Change the endpoint through an environment variable

    Your application must read the endpoint, region and keys from its environment, never from code. Change these variables, restart the service, then test an upload, a read and a deletion on a test object. Regenerate the presigned links that matter.

  5. Monitor, and only then dismantle

    Leave MinIO in place, stopped or read-only, long enough to see a full cycle of your backups and scheduled jobs go by. Point your restic backups at the target again, see the guide on restic backups.

Rolling back comes down to one variable

As long as MinIO has not been deleted and the last sync was done in read-only mode, going back means putting the old endpoint in the environment and restarting. If writes have happened on the target since the switch, bring them back to the source first with an rclone copy in the other direction (never a sync, which would delete), then go back. Erase MinIO only after an observation period that you set in advance.

Troubleshooting: the symptoms that keep coming back

An unsigned call on Garage answers 403 with the message Garage does not support anonymous access yet. That is the expected behaviour of Garage, and the request for anonymous access is still in the project tracker as a feature that was never prioritised. If your application served public files without a signature, you will need presigned URLs or a proxy in front of the bucket.

If writes are rejected with signature errors right after the switch, compare the client's region with the target's (s3_region for Garage, RUSTFS_REGION for RustFS) and check that the client uses path-style addressing. If rclone check reports multipart objects as different although their sizes are equal, replay it with --download. If a rerun of mc mirror does not copy a modified object, the cause is the missing --overwrite. Finally, if a bucket is empty on the target although the sync succeeded, check that the key was given --read --write on that bucket on the Garage side, or the right policy on the RustFS side.

The official documentation

Check these sources again before acting, because these projects move: the github.com/minio/minio repository for MinIO's status and licence; github.com/rustfs/rustfs and docs.rustfs.com for RustFS; garagehq.deuxfleurs.fr for Garage's S3 compatibility; rclone.org for sync and check; MinIO's documentation for mc mirror. To back up this storage, see the 3-2-1 article with restic and S3.

A second VPS to test the target before cutting off MinIO

A second root VPS with an NVMe disk hosts the target during the migration: you test alongside production and only cut MinIO once the object counts are validated. The switch happens without touching the original machine.

Need help?

Browse our help center and FAQ, or reach our team — callback, WhatsApp or email. Support in French, English and Arabic.

Message us on WhatsAppopens in a new tab