Migrating from MinIO to Garage: A Complete Guide
MinIO's community edition is archived. This is the actual migration to Garage: the rclone commands, the credential and region gotchas that look like deeper problems, and what silently doesn't come across.
MinIO's community edition is archived as of April 2026: read-only, no patches, no releases. If you're running it, the deployment still works today. But nothing in it will get fixed when it breaks or drifts from the standard.
This is the migration: what breaks moving to Garage, why, and the exact commands to do it without losing anything or taking your app down for more than a few minutes.
The good news: this is a solved problem
S3-compatible storage is, by design, easy to move between. Your app talks to an endpoint with a key and a secret. It doesn't know or care whose engine is answering. Moving from MinIO to Garage (or anything else that speaks S3) is not a rewrite. It's swapping which door your requests go through.
The tool for the actual data transfer is rclone. It's been the standard for this exact job for close to a decade, and it's the same tool regardless of which S3-compatible engine you're moving to.
Don't have Garage running yet?
This guide assumes a Garage instance already exists as your destination.
If you don't have one yet: Garage's own quickstart docs will get you a working node. If you'd rather watch someone set one up end to end, I recorded setting up a Garage node, or read the write-up on my personal blog mathewstorm.ca.
Snags you might hit
Before the commands, the parts that trip people up. None of these are hard once you know they're coming. All of them look like something deeper is broken if you don't.
Your old access key doesn't just carry over
MinIO lets you set MINIO_ACCESS_KEY to whatever string you want. Garage generates its own key IDs, prefixed GK, and it won't accept an arbitrary one you hand it. You're not migrating your key. You're
generating a new one on the destination and updating your app's config to match. If your migration plan assumed the credentials would just work after a copy-paste, that's the moment it breaks.
"Access Denied" right after switching endpoints is almost never a real permissions problem
This is the single most common thing people hit, and it's almost always one of two mundane causes: the new endpoint expects AWS Signature v4 and something in your client is still configured for an older signing method, or you copied the wrong half of a credential pair. Before you go looking for a real access-control issue, check those two things first.
A clean sync doesn't mean everything came across - check what Garage can't hold
Garage has no object versioning, no object lock, and no bucket policies. This isn't something rclone will surface for you. It's a gap that copies silently: rclone sync will report success either way, because none of these are things it errors on.
-
Versioning. By default, rclone only ever looks at the current
version of each object. If you've had versioning enabled on MinIO,
every prior version of every file stays behind on the old bucket,
untouched and unreported. If you need that history, pull it out
explicitly first withrclone copy --s3-versionsbefore you
decommission anything - once MinIO's gone, so is the history. -
Object Lock / retention. A locked object copies over fine - the
lock only blocks delete/overwrite on the source, not reading it. What
doesn't come across is the lock itself. The file lands on Garage as an
ordinary, fully mutable object, with whatever compliance or
ransomware-protection guarantee it had on MinIO now gone. If anything
in your bucket was locked for a reason, that reason needs a different
answer post-migration - Currently, Garage has no equivalent feature. -
Bucket policies. These are bucket-level configuration, not object
data, sosyncnever touches them - they're simply left behind. Any
access control you had scoped through MinIO policy needs to be rebuilt
some other way on the new side, whether that's Garage's simpler
per-key bucket grants or a rule enforced somewhere else entirely.
None of this will show up as an error. It shows up as a bucket that
looks fully migrated and isn't. This is important to remember if you were depending on any of these.
The actual migration
This guide keeps your app up the whole time, with the old
system as a safety net until you've confirmed the new one is solid.
1. Stand up your destination. Have your Garage bucket ready. Generate
a fresh access key there:
garage key create migration-key
Garage keys start with zero bucket permissions - this key can't touch
anything yet. Grant it access to just the destination bucket:
garage bucket allow --key migration-key --read --write your-bucket
Skip this and you'll get AccessDenied the moment you try to sync,
looking like a real permissions problem when it's just a missing grant.
Moving more than one bucket? Run that same allow line once per bucket,
same key - garage bucket allow --key migration-key --read --write
other-bucket, and so on. You don't need to touch the rclone config again
either: it only stores the key, not a specific bucket, so the same
garage_remote works against garage_remote:bucket-one,
garage_remote:bucket-two, or any bucket you've granted that key access
to - the bucket name is something you type at sync time, not something
baked into the remote.
Garage has no separate "admin key" that gets everything automatically,
including future buckets - every grant is explicit, per key, per bucket.
If you want one key that behaves like an admin key for your own buckets,
that's just granting it on all of them; nothing implicit happens on its
own.
On the MinIO side, if you have the option, use a read-only key scoped to
just the source bucket rather than your root/admin credentials - this
key only needs to read, and only from one place, for one migration.
2. Alias both endpoints in rclone.
A note on the endpoints first. MinIO can terminate TLS itself if you've
given it certificates, so https://your-minio-host:9000 may be exactly
right as-is. Garage's S3 API does not support TLS on its own - Garage's
own docs are explicit about this, api_bind_addr (port 3900 by default)
is plain HTTP, full stop. If you're following the Caddy setup from the
Garage guide above, your real destination endpoint is your domain over
standard HTTPS (https://your-garage-domain, no port needed), not
your-garage-host:3900 directly. You'd only hit 3900 with https:// if
you've put your own TLS-terminating proxy directly in front of that
port instead of a domain on 443 - most setups won't.
rclone config create minio_remote s3 \
provider=Minio \
access_key_id=YOUR_MINIO_KEY \
secret_access_key=YOUR_MINIO_SECRET \
endpoint=https://your-minio-host:9000
rclone config create garage_remote s3 \
provider=Other \
access_key_id=YOUR_GARAGE_KEY \
secret_access_key=YOUR_GARAGE_SECRET \
endpoint=https://your-garage-domain \
region=garage \
force_path_style=true
Two easy-to-miss params on the Garage side. region has to match what's
set as s3_region in your garage.toml - most setups leave this at the
default, garage, but if yours was changed, use that value instead.
force_path_style=true matters because Garage expects path-style
requests (endpoint/bucket/key) rather than AWS's default virtual-host
style (bucket.endpoint/key). Virtual-host style is possible on Garage
but requires wildcard DNS and a wildcard certificate - real setup work
most self-hosters running a personal data store haven't done. Leave this
true unless you know you've configured vhost-style yourself.
3. Run the first sync, while your app is still live on MinIO:
rclone sync minio_remote:your-bucket garage_remote:your-bucket \
--transfers=16 \
--checkers=16 \
--progress
For a large bucket this can take a while. That's expected, not stuck.
--transfers and --checkers are the two flags to raise if it
feels slow on a big flat key space with a lot of small files.
4. Verify before you trust it:
rclone check minio_remote:your-bucket garage_remote:your-bucket
This compares size and hash for every object on both sides and reports anything that doesn't match or is missing - catching a sync that silently dropped or corrupted something, which a "finished with no errors" message alone won't tell you.
5. Cut your app over. Point it at the new endpoint and the new
credentials. Nothing else about your application code should need to
change, that's the entire premise of S3 compatibility holding.
6. Run one more incremental sync, to catch anything written to the
old bucket in the gap between your last sync and the cutover:
rclone sync minio_remote:your-bucket garage_remote:your-bucket --progress
Since it's incremental, this one should be fast, mostly confirming
nothing changed.
7. Verify again. Same rclone check as before. If it's clean, you're
done moving data.
8. Watch it for a day or two before you decommission MinIO. Keep the
old bucket around, untouched, as your rollback until you're confident.
Then shut it down.
What you didn't have to do
Notice what's absent from that list: no rewritten application code, no
new API to learn, no vendor migration tool, no export ticket. The whole
reason this works is that S3 protocol that both sides could speak. Moving between two things that
speak it is what "no lock-in" is supposed to mean.
Don't want to run and patch your own Garage node for the next several
years? We do that part. Storm Buckets is Canadian-hosted, S3-compatible
object storage built on Garage, in open alpha with 100GB free.