How to migrate from MinIO to Garage with Docker
Table of contents
- Key takeaways
- What happened to the MinIO images on Docker Hub?
- Is switching to the quay.io image enough?
- What Garage lacks and when it is the wrong choice
- How I tested the migration
- Step 1: run Garage next to MinIO
- Step 2: create the keys and buckets in Garage
- Step 3: copy the data with rclone sync
- Step 4: verify the copy with rclone check
- Step 5: switch the clients to Garage
- What you lose when you migrate from MinIO to Garage
- How much memory and disk MinIO and Garage use
- Frequently asked questions
- Can I keep running the MinIO image from quay.io?
- Is Garage suitable for immutable backups?
- Do I need more than one Garage node?
- Conclusion
- Sources
MinIO images no longer pull from Docker Hub, and the quay.io copy is frozen in 2025. To migrate from MinIO to Garage with Docker, run Garage v2.4.1 next to MinIO, copy the buckets with rclone sync, verify them with rclone check, and switch endpoint, keys and region in your clients.
docker pull minio/minio no longer works: MinIO’s Docker Hub repositories disappeared in September 2026. If you set up your storage with the guide to installing MinIO with Docker, you can keep pulling the quay.io copy for now, but that image will not be updated again. This guide shows how to migrate from MinIO to Garage with Docker: run Garage v2.4.1 next to MinIO, copy the buckets with rclone, verify them and switch the clients. I ran the whole thing with 9,996 objects and report how long it took, what failed and what got left behind.
Key takeaways
- Docker Hub returns 404 for
minio/minioandminio/mc(checked on 16 September 2026). The removal date, between 9 and 12 September, comes from third-party reports: MinIO has not announced it. quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Zstill pulls, but it is frozen and does not include the fix for CVE-2025-62506, which has a CVSS score of 8.1.- Garage has no versioning, no object lock, no bucket policies and no object tags. If your backups depend on any of those features, this migration is not for you.
- With rclone 1.75.1, copying 9,996 objects (1,018.5 MiB) from MinIO to Garage took 41.99 s at the median, and a verification that downloaded both sides found no differences.
- The final
rclone syncpass must not use--checksum: with that flag, rclone accepted a 20 MiB copy that had changed in MinIO.
What happened to the MinIO images on Docker Hub?
The minio/minio and minio/mc repositories no longer exist on Docker Hub. On 16 September 2026 the Docker Hub API answered 404 with object not found for both, and pulls failed with every tag, including the one the install guide pinned:
$ docker pull minio/minio:RELEASE.2025-09-07T16-13-09Z
Error response from daemon: pull access denied for minio/minio,
repository does not exist or may require 'docker login'
MinIO has published no notice, so the date comes from third parties. A collaborator on dandi-cli places the removal[1] between 9 and 12 September 2026, with a measured window of 18:12 to 20:13 UTC on the 11th. An issue opened in Milvus[2] on 14 September describes the same breakage in its development environment.
The rest of the story can be checked. The MinIO repository on GitHub[3] has been archived since 25 April 2026 (the Milvus issue says 13 February, but GitHub is the record). Its README opens with "THIS REPOSITORY IS NO LONGER MAINTAINED" and explains that the community edition now ships as source code only.
Is switching to the quay.io image enough?
It is enough to get your MinIO running again today, not as a destination. The quay.io copy is still available, and there the RELEASE.2025-09-07T16-13-09Z tag and latest point to the same digest (sha256:14cea493), dated 7 September 2025. It has an arm64 variant, and it pulled here in 3.2 s. If your compose file still uses the Docker Hub name, change it and recreate the service:
sed -i 's#image: minio/minio:#image: quay.io/minio/minio:#' \
docker-compose.yml
docker compose pull minio && docker compose up -d minio
That image carries a known security flaw. The last release published on GitHub, RELEASE.2025-10-15T17-29-55Z[4], fixes CVE-2025-62506[5]: a service account with a restricted policy can create another one with all of its parent’s permissions. The advisory rates it high, at 8.1 out of 10.
There is no quay.io image for that release. The release notes say so plainly: "For container environments, please clone the source and build the latest container". That leaves two paths: build MinIO yourself, or leave it.
What Garage lacks and when it is the wrong choice
Garage is an object storage server compatible with the Amazon S3 application programming interface (API), written in Rust and released under the AGPLv3. It is developed by Deuxfleurs, a small self-hosted service provider that has run it in production since 2020.
Garage handles reads, writes, listings, multipart uploads, presigned URLs and static websites. What is missing is listed in its S3 compatibility table[6]. On access control lists (ACL) and policies, it says: "Garage implements none of them, and has its own system instead, built around a per-access-key-per-bucket logic".
I tested the first five rows with the mc client against Garage v2.4.1; the last two come from the documentation:
| Feature you had in MinIO | Garage v2.4.1 | What it answered |
|---|---|---|
| Object versioning | No | 501 on PutBucketVersioning |
| Object lock and retention | No | 501 on PutObjectLockConfiguration |
| Bucket policies and anonymous download | No | 501 on GetBucketPolicy |
| Object tags | No | 501 on GetObjectTagging |
Administration with mc admin |
No | 400 Invalid endpoint: metrics |
| Lifecycle rules | Expiration and multipart cleanup only | Per the documentation |
| Web console | No | Command line and admin API |
If your backups rely on object lock to stay immutable, or an application reads older versions of an object, stay on a server that has those features. For application files, restic repositories, rclone copies or static sites, Garage covered everything I tested.
How I tested the migration
I ran MinIO RELEASE.2025-09-07T16-13-09Z (from quay.io) and Garage v2.4.1 in a throwaway Compose project, inside a linux/arm64 development container with 18 cores. The machine was shared with other workloads and the load average ranged from 22 to 39 during the measurements, so treat the timings as rough reference points, not as a benchmark.
With mc mirror I loaded a MinIO bucket called app-data with 9,996 objects and 1,018.5 MiB:
- 6,000 JSON documents between 2 and 40 KB
- 3,990 random files between 4 and 256 KB
- 5 identical copies of a 20 MiB file
- 1 file of 300 MiB, which
mcuploaded in 19 parts
I added one object with Cache-Control, a custom metadata field and two tags. I also created a versionado bucket with 6 versions of the same object, a publico bucket with anonymous download and a restic 0.19.1 repository with one snapshot. For the copy I used rclone 1.75.1, the stable release of 4 September 2026. To load the data I used mc RELEASE.2025-08-13T08-35-41Z, the last client image left on quay.io.
Step 1: run Garage next to MinIO
Garage needs a configuration file, two folders and two secrets, and it should start in the same Compose project as MinIO so rclone can reach both over the same network. This is the garage.toml I used:
metadata_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"
db_engine = "lmdb"
metadata_auto_snapshot_interval = "6h"
replication_factor = 1
rpc_bind_addr = "[::]:3901"
rpc_public_addr = "127.0.0.1:3901"
[s3_api]
s3_region = "us-east-1"
api_bind_addr = "[::]:3900"
root_domain = ".s3.garage.localhost"
[s3_web]
bind_addr = "[::]:3902"
root_domain = ".web.garage.localhost"
index = "index.html"
[admin]
api_bind_addr = "[::]:3903"
It differs from the Garage quick start[7] in two places. The first is the region, us-east-1 instead of garage, for a reason I explain when switching the clients. The second is the data path, /var/lib/garage instead of /tmp, so the data outlives the container. The setting replication_factor = 1 is single-node mode, and the guide itself warns that such a deployment "should not be used in production" because it keeps no redundant copy.
Both secrets go in the project’s .env, generated with openssl, as covered in environment variables and secrets in Docker Compose:
cat >> .env <<EOF
GARAGE_RPC_SECRET=$(openssl rand -hex 32)
GARAGE_ADMIN_TOKEN=$(openssl rand -base64 32)
EOF
Then add the service to your docker-compose.yml, below the MinIO one:
garage:
image: dxflrs/garage:v2.4.1
command: ["/garage", "server", "--single-node"]
restart: unless-stopped
environment:
GARAGE_RPC_SECRET: ${GARAGE_RPC_SECRET}
GARAGE_ADMIN_TOKEN: ${GARAGE_ADMIN_TOKEN}
ports:
- "3900:3900"
- "3902:3902"
- "127.0.0.1:3903:3903"
volumes:
- ./garage.toml:/etc/garage.toml:ro
- ./garage/meta:/var/lib/garage/meta
- ./garage/data:/var/lib/garage/data
The --single-node flag, available since v2.3.0, creates the cluster layout on first start and keeps it on later ones. Both folders are host mounts (the article on Docker volumes and bind mounts explains them): meta holds the LMDB database and data holds the blocks. The Garage deployment guide[8] points out that LMDB files cannot move between CPU architectures, so do not copy meta from an x86 server to an arm64 one.
Start it with docker compose up -d garage and check the node with garage health, a subcommand that arrived with v2.4.0[9] on 6 September 2026. Version 2.4.1 followed two days later and only fixes a crash at startup with Consul or Kubernetes discovery. The output, trimmed:
$ docker compose exec garage /garage health
Cluster health: HEALTHY
Known nodes: 1
Storage nodes up: 1
Partitions: 256
Partitions with quorum: 256
Fully healthy partitions: 256
Step 2: create the keys and buckets in Garage
Garage has no root user: each access key gets permissions bucket by bucket. Create a key for the migration and one bucket per MinIO bucket, with the same name:
alias garage='docker compose exec -T garage /garage'
garage key create migracion
garage bucket create app-data
garage bucket allow --read --write --owner app-data --key migracion
garage key create prints an ID that starts with GK and a secret of 64 hexadecimal characters. Write them down: rclone will use them.
If you would rather not touch each application’s credentials on switch-over day, garage key import also accepts MinIO keys. Without --yes it refuses with this warning: "This command is intended to re-import keys that were previously generated by Garage". With --yes, v2.4.1 accepted a MinIO key without the GK prefix, and restic worked with it without trouble.
That import has two limits: the key ID needs at least 8 characters and the secret at least 16. That is why the admin user from the install guide was rejected, with the error Key identifiers should be at least 8 characters long. Treat it as a bridge for switch-over day and rotate those keys afterwards:
garage key import --yes -n app-antigua \
minio_access_key_id minio_secret_key
garage bucket allow --read --write app-data --key app-antigua
Step 3: copy the data with rclone sync
rclone copies from one S3 server to another without going through local disk, and the Garage client documentation[10] gives a template with provider = Other. This rclone.conf declares both ends using the Compose service names:
[minio]
type = s3
provider = Minio
access_key_id = minio_root_user
secret_access_key = minio_root_password
endpoint = http://minio:9000
[garage]
type = s3
provider = Other
access_key_id = migration_gk_key_id
secret_access_key = migration_secret_key
region = us-east-1
endpoint = http://garage:3900
force_path_style = true
The copy runs in a container attached to the project network. Compose names it <folder>_default; in this example the folder is minio, and docker network ls gives you the real name:
docker run --rm --network minio_default \
-v "$PWD/rclone.conf:/config/rclone/rclone.conf:ro" \
rclone/rclone:1.75.1 sync minio:app-data garage:app-data \
--metadata --fast-list --transfers 8 --checkers 16 -v
I repeated the full copy three times, wiping Garage between runs. It took 53.42 s, 40.11 s and 41.99 s (about 24 MiB/s at the median), with a load average between 25.7 and 38.5 on 18 cores. All three finished with 9,997 objects copied and no errors. The --fast-list flag cuts down listing calls, as the Garage documentation advises, and --metadata keeps the headers: the test object arrived with Cache-Control: max-age=3600 and X-Amz-Meta-Autor: ana.
While MinIO is still in service, you can repeat sync as many times as you like. With no changes at the source, one series of three passes took between 2.43 and 2.52 s. An earlier series took between 4.25 and 21.43 s under the same background load. With 10 objects modified and 1 deleted in MinIO, the pass replaced the 10, deleted the extra one in Garage and finished in 6.63 s.
Step 4: verify the copy with rclone check
rclone check compares the size and MD5 sum of each object, but not every object has a comparable sum. The first verification returned this:
NOTICE: S3 bucket app-data: 0 differences found
NOTICE: S3 bucket app-data: 6 hashes could not be checked
NOTICE: S3 bucket app-data: 9997 matching files
The 6 unchecked objects are the ones mc uploaded in parts: the 5 copies of 20 MiB, whose ETag header ends in -2, and the 300 MiB file, which ends in -19. That ETag is not the MD5 of the content, so rclone can only compare the size. The objects I wrote later with mc pipe added another 10 to the list.
I tested that gap on purpose. I replaced one of the 20 MiB copies in MinIO with a different file of the same size and ran rclone sync --checksum: it copied nothing, and rclone check again reported 0 differences found. The pass without --checksum, which also compares the modification time, did copy it. That is why the verification that counts is the one that downloads both sides:
docker run --rm --network minio_default \
-v "$PWD/rclone.conf:/config/rclone/rclone.conf:ro" \
rclone/rclone:1.75.1 check minio:app-data garage:app-data \
--download --fast-list --checkers 16
With 1,018.5 MiB, the check with --download[11] took 20.44 s at the median over three passes, with a load between 33 and 37. All three ended with 9996 matching files and no differences.
Step 5: switch the clients to Garage
Each client needs three changes: the address (port 3900 instead of 9000), the credentials, unless you imported the MinIO ones, and the region.
The region is the trap that gives no warning. MinIO without a configured region accepted signatures for us-east-1, eu-west-1 and garage, so a client may have carried any value for years. Garage accepts only its own. With the s3_region = "garage" from the official guide, rclone configured with us-east-1 got this:
api error AuthorizationHeaderMalformed: Authorization header
malformed, unexpected scope: '20260916/us-east-1/s3/aws4_request',
expected: '20260916/garage/s3/aws4_request'
Check which region each client uses and put that one in s3_region. In my test it was us-east-1: changing it in garage.toml and recreating the container fixed it without touching the client, and afterwards the garage region failed the same way. The data and the node layout survived the change.
For restic, the client with the most to lose, I also copied its bucket with rclone and pointed the repository at Garage with the imported MinIO key. If you do not use it yet, the guide to installing restic for encrypted backups covers the rest:
export RESTIC_REPOSITORY=s3:http://garage:3900/restic
restic snapshots
restic check --read-data
restic snapshots listed the snapshot taken on MinIO and check --read-data finished with no errors were found. The next backup, now against Garage, added 1.018 KiB to the repository, because the rest of the data was already there. There was no need to set the region: restic detects it on its own.
With the clients tested, the final switch goes in this order:
- Stop the applications that write to MinIO.
- Run the last
rclone sync, without--checksum. - Run
rclone check --download. - Change the address and credentials of each application and start them.
- Leave MinIO stopped, without deleting its data, until you confirm everything works.
What you lose when you migrate from MinIO to Garage
rclone copies the current version of each object with its headers, and nothing else. This is what stayed behind in MinIO during the test:
- Older versions: the
versionadobucket held 6 versions ofconfig.txt, and Garage ended up with 1, the latest - Object tags: they vanished without rclone writing any warning to its log
- Anonymous download: the same
logo.jpgthat MinIO served without credentials returned 403 on Garage - MinIO users and policies: they are not copied; in Garage they are replaced by keys with per-bucket permissions
Public download has a replacement. garage bucket website --allow publico turns on website mode for the bucket, which Garage serves on port 3902 based on the host name. With the header Host: publico.web.garage.localhost, the same request returned 200 and the file’s 153,711 bytes. In production, your reverse proxy resolves that name.
How much memory and disk MinIO and Garage use
Garage used less memory and less disk for the same data. These figures come from docker stats and du during the test:
| Measurement | MinIO 2025-09-07 | Garage v2.4.1 |
|---|---|---|
| Idle memory, with the data loaded | 452 MiB | 22.6 MiB |
| Peak memory during each copy | 520, 473 and 461 MiB | 66, 159 and 47 MiB |
| Disk for the four buckets | 1,074 MiB | 884 MiB (859 of blocks and 25 of metadata) |
| Compressed arm64 image | 57.5 MB | 27.0 MB |
The disk difference has two documented causes. Garage splits each object into 1 MiB blocks and stores repeated blocks once, so the 5 copies of 20 MiB take the space of one. It also compresses the blocks with zstd, at level 1 by default, and the 6,000 JSON documents compress well. With data that is already compressed, such as photos or video, the advantage shrinks to deduplication.
Frequently asked questions
Can I keep running the MinIO image from quay.io?
You can, but without updates and without the fix for CVE-2025-62506. If you use service accounts or temporary credentials with restricted policies, that vulnerability affects you, and the only official way out is to build RELEASE.2025-10-15T17-29-55Z yourself.
Is Garage suitable for immutable backups?
No. Garage returns 501 to the object lock and versioning calls, so it cannot stop someone holding the key from deleting or overwriting a backup. For that you need another server, or encrypted copies in a second destination the first one cannot touch.
Do I need more than one Garage node?
Not to make it work: everything in this guide ran on a single node. But one node keeps no redundant copies, and the Garage documentation recommends at least three nodes for cluster mode, with the three copies of each piece of data in different locations. I did not test a cluster, because on a single machine it proves nothing.
Conclusion
Migrating from MinIO to Garage with Docker takes five steps, none of them long. Garage starts from a 20-line file, rclone copies 1,018.5 MiB in about 42 s and rclone check --download confirms the result. What decides whether you can migrate is not the procedure but the features you use: without versioning, object lock or policies, Garage is not for everyone. If your buckets hold application files or restic repositories, run the copy in parallel first, get the region right and skip --checksum on the final pass.
The Spanish version of this guide is at Cómo migrar de MinIO a Garage con Docker.
Sources
- dandi-cli places the removal
- issue opened in Milvus
- MinIO repository on GitHub
- RELEASE.2025-10-15T17-29-55Z
- CVE-2025-62506
- S3 compatibility table
- Garage quick start
- Garage deployment guide
- v2.4.0
- Garage client documentation
- check with --download
- Quay, minio/minio RELEASE.2025-09-07T16-13-09Z tag
- Garage, v2.4.1 release notes
- Garage, configuration reference