MinIO Is Dead: Migrate to RustFS Without Losing Data
Foreword
MinIO stopped maintenance just like that, but the online file service still has to keep running.
In December 2025, MinIO officially announced that the Community Edition would enter maintenance mode. In February 2026, the repository was archived as read-only: no more commits, CVEs would no longer be fixed, and official resources were fully shifted to the commercial version AIStor—a subscription model that personal projects cannot afford. Deployments still running the Community Edition are essentially holding onto a black box that will no longer be updated.
The file storage for youlai-boot (a Spring Boot permission management backend) has always run on MinIO. Taking this opportunity, the migration was completed end-to-end: old data was copied as entire directories, the endpoint and bucket name remained unchanged, and the client was conveniently switched to the AWS SDK standard implementation. The process is equally applicable to any Docker-deployed MinIO—including the supporting environment for vue3-element-admin.
Highlights of this article:
- Selection criteria: Which scenario suits RustFS / SeaweedFS / Garage
- Standard client: Switching to AWS SDK v2, following the protocol, not the vendor
- Zero data loss: Copy old directories entirely, keeping old data intact as a backup
I. Environment Preparation
1.1 MinIO End-of-Life Timeline
Let's look at the facts first, then discuss migration:
| Time | Event |
|---|---|
| 2025-12 | Official announcement that the Community Edition enters maintenance mode: no new features or PRs, security fixes "evaluated on a case-by-case basis" |
| 2026-02 | GitHub repository archived as read-only, README marked "THIS REPOSITORY IS NO LONGER MAINTAINED" |
| Afterward | Official resources fully shifted to the commercial version AIStor; disclosed CVEs in the Community Edition will no longer be fixed |
1.2 How to Choose an Alternative
There are four mainstream open-source alternatives. Here is the conclusion table first:
| Solution | Protocol | Status | Suitable Scenario |
|---|---|---|---|
| SeaweedFS | Apache 2.0 | Production-proven for 10 years | Massive small files, battle-tested |
| RustFS | Apache 2.0 | Actively iterating, relatively new | Single-container deployment, lowest MinIO migration cost |
| Garage | AGPL v3 | Production-ready | Geographically distributed clusters |
| Ceph RGW | LGPL | Enterprise-grade | PB-scale, too heavy for a single machine |
All four candidates claim S3 compatibility, so this is not a differentiator. The real watershed is the deployment mental model:
- SeaweedFS is the most mature, but it has a multi-role architecture (Master / Volume / Filer). Migrating from MinIO requires re-understanding a new deployment model.
- RustFS is a single container with one
docker runcommand, using ports 9000 for the API and 9001 for the console, completely consistent with MinIO usage habits. - RustFS also directly supports the MinIO disk format—old data can be copied as entire directories without going over the network (see Section 2.2). Other candidates require network-based transfers.
I chose RustFS because the migration path is the shortest: the deployment method, port conventions, and console operations are all continued. Even the disk data format is compatible. The team doesn't need to learn anything new, and the application side only needs to change a pair of AK/SK at minimum.
Boundaries must also be clarified: RustFS is younger than SeaweedFS and lacks the same depth of production validation. For PB-scale core storage, SeaweedFS or Ceph is more stable; for single-machine personal use or small-to-medium scale, the migration cost vote goes to RustFS.
1.3 Pre-Migration Checks
# Docker environment
docker -v
# Disk space: Section 2.2 requires copying entire directories, needing about 2x the space of the old data
df -h /data
# Record the old MinIO data directory path (used for copying data in Section 2.2, example in this article is /data/minio)
docker inspect minio --format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
Note down the old data directory path from the last command—it corresponds to the new directory /data/rustfs at the same level. Section 2.2 copies data from it.
II. Migration Preparation
A diagram of the entire migration process:
The process does only one thing: Data lands first, then the service starts. There is no back-and-forth of "start first, then migrate data"—the container starts only once, and commands are not repeated.
2.1 Stop MinIO
# Delete the container, keep the data directory—you can roll back anytime before the migration is verified
docker stop minio && docker rm minio
2.2 Prepare the Data Directory
The old data is in /data/minio (the path found in Section 1.3). Copy the entire directory to /data/rustfs—the two directories are independent and at the same level. The old directory is kept as-is, naturally serving as a rollback backup.
# Remove any potentially existing empty directory: the copy requires the target directory not to exist
rm -rf /data/rustfs
# Copy the entire old directory to the new directory: -a preserves permissions and timestamps
cp -a /data/minio /data/rustfs
# The copy's owner is still root (old MinIO ran as root), align it with RustFS's runtime identity
chown -R 10001:10001 /data/rustfs
Why directly copying the disk directory works: First, correct an intuition—MinIO does not store "one object per file" on disk. What you see with ls /data/minio/public are not images, but a bunch of directories with the same names as objects (containing xl.meta metadata and shards). So, picking out "image files" to copy won't work. But copying the entire directory does work: Since version 1.0.0-alpha.89, RustFS supports the MinIO disk format (officially called a drop-in replacement, issue #2212). Buckets, objects, and bucket policies are all recognized, and data doesn't travel over the network. Moving TB-scale data over the network takes hours, while directory copying is only limited by disk speed (reference: 2.3 TB Production Migration Record).
Three things to keep in mind about the copied data:
- Access Keys are not migrated with the data (community feedback): Don't expect old keys to work. Use the new AK/SK from the startup environment variables uniformly—so the application configuration in Chapter IV still needs to be changed.
- Compatibility boundaries: Bucket metadata, objects (including tags/versions/object locks), lifecycle, and IAM are within the migration scope; site replication, event notifications, and LDAP/OIDC are not.
- Don't delete the old directory yet: Clean it up only after confirming the verification in Section 3.2 passes. Until then, it's a ready-made rollback backup.
III. Start RustFS
3.1 Start the Container
The data is in place. This step is just about getting the service running:
docker run -d \
--name rustfs \
--restart unless-stopped \
-p 9000:9000 \
-p 9001:9001 \
-v /data/rustfs:/data \
-e RUSTFS_ACCESS_KEY=rustfs-admin \
-e RUSTFS_SECRET_KEY='Replace with your strong password' \
rustfs/rustfs:latest
The port meanings are completely consistent with MinIO: 9000 is the S3 API (applications connect here), and 9001 is the Web console. If your cloud server security group already had rules for MinIO, you don't need to change a single port.
The data directory owner must be 10001. The official MinIO image runs as root and can write anywhere it's mounted. The RustFS image declares USER 10001 in its Dockerfile, meaning the container process runs as this non-root identity. Bind mounts do not translate permissions—when UID 10001 inside the container writes to /data, the kernel checks against the host directory's owner. If the owner is not aligned, the container crashes immediately on startup:
# Crashes on startup: [FATAL] Server runtime failed: Io error: Permission denied (os error 13)
Section 2.2 already executed chown 10001 on the copy, so it can start normally here.
Two common questions. Why not use
chmod 755? 755 only gives the owner write permission; UID 10001 falls into the "others" category and still cannot write. Usingchmod 777bypasses this but effectively opens write permissions to all users on the server. Where does 10001 come from? You can check withdocker inspect rustfs/rustfs:latest --format '{{.Config.User}}'—it's an identity in the ordinary user range (10000-60000) declared by the image author. Official issue #2396 confirms this is a standard requirement for non-root containers.
3.2 Verify the Service
docker ps | grep rustfs # Status Up indicates success
docker logs -f rustfs # No errors in logs
Open http://<ServerIP>:9001 in a browser and log in using the AK/SK set at startup—the console should directly show the old MinIO buckets and files.
3.3 Enable Anonymous Read
Old bucket policies are carried over with the data; if it's a newly created bucket (or the old one never had it enabled), anonymous downloads need to be enabled for scenarios like blog image hosting or frontend direct links. The operation path has two steps:
- In the left menu, click the first item Object Browser → Bucket List → Find the target bucket, click the Settings button at the end of the row.
- After entering the bucket details, under Access & Sharing → Access Policy, click Edit, change it to Public, and save. A "Modification successful" prompt indicates it's effective.
If not enabled, all external image links will return 403. Note that "Public" means read-only anonymous access—external links can fetch images, but uploads still require AK/SK authentication, keeping security manageable.
IV. Application Switch
4.1 Switch to AWS SDK
io.minio:minio is an S3 protocol client library. It still works now, but its lifeline is tied to a company that has fully shifted to a commercial version. The AWS SDK for Java v2 is the de facto standard in the S3 ecosystem, officially supporting connections to any S3-compatible storage. Since we're migrating anyway, switch together and never have to choose an SDK again:
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<!-- Latest version at time of writing, check Maven Central for the actual one -->
<version>2.54.2</version>
</dependency>
Complete implementation source code: RustFSFileServiceImpl.java
file-storage:
type: s3 # Enum changed to s3: follow the protocol, not the vendor
s3:
endpoint: http://<ServerIP>:9000
access-key: rustfs-admin # RustFS's new AK
secret-key: Replace with your strong password # RustFS's new SK
bucket: public # Data has been copied over, the bucket is still the same
import software.amazon.awssdk.auth.credentials.*;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
@Bean
public S3Client s3Client(FileStorageProperties props) {
var s3 = props.getS3();
return S3Client.builder()
// Connect to your own server, not AWS
.endpointOverride(URI.create(s3.getEndpoint()))
// Required for non-AWS services, value can be arbitrary
.region(Region.US_EAST_1)
// Required for IP endpoints: bucket name goes in the path, not the subdomain (see pitfalls list)
.forcePathStyle(true)
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(s3.getAccessKey(), s3.getSecretKey())))
.build();
}
The upload call changes from MinioClient to S3Client.putObject, with parameters mapping one-to-one. The changes are concentrated in a single Service implementation class (RustFSFileServiceImpl.java).
The judgment criterion in one sentence: Protocol clients follow the protocol, not the vendor. The S3 protocol is an open standard defined by AWS and will outlive any single object storage company.
4.2 Verify Upload
# Restart the application
mvn spring-boot:run
Method 1: Open http://localhost:8000/doc.html, call the file upload interface. If the interface returns a file URL, it's successful.
Method 2: Open the vue3-element-admin frontend, go to "Component Encapsulation → Image Upload", select an image to upload; you can also directly access the Youlai online environment: https://vue.youlai.tech/#/component/upload
After a successful upload, copy the returned file URL and open it in a browser—if the image displays normally, the link from the backend to RustFS is working.
Finally, go back to the RustFS console. You should see the newly uploaded image file in the
public bucket, completing the end-to-end migration verification.
V. Pitfall Checklist
| Pitfall | Symptom | Solution |
|---|---|---|
| Incorrect directory owner | Crashes on startup, Permission denied (os error 13) |
RustFS runs as non-root UID 10001; chown -R 10001:10001 the data directory (official issue #2396) |
| Old keys invalid | Upload with old AK/SK reports 403 AccessDenied | Old MinIO Service Accounts are not migrated; uniformly switch to the new AK/SK set at RustFS startup |
| SDK path-style not enabled | Request domain resolves to bucketname.ServerIP, connection fails |
Set S3Configuration.pathStyleAccessEnabled(true) in AWS SDK (old method name forcePathStyle); required for IP endpoints |
| Bucket does not exist | Application upload reports NoSuchBucket | Old buckets are carried over with the data copy; manual recreation is only needed when deploying on a completely new empty directory |
| Copying files directly | Objects don't appear after copying: MinIO stores each object as a directory (xl.meta + shards), not a file | Copy the entire directory with cp -a; RustFS supports this format (≥ 1.0.0-alpha.89) |
| New SDK signature error | XAmzContentSHA256Mismatch | Old MinIO doesn't support streaming signatures and will never be fixed—time to migrate |
| External image links get 403 | Anonymous fetch denied | Set the bucket's access policy to "Public" (read-only anonymous access), see Section 3.3 |
The root cause of the owner issue is the difference in image security models: the official MinIO image runs as root and can write anywhere mounted; RustFS runs as a non-root user with UID 10001, so the host directory must be chown 10001 before starting (confirmed by official issue #2396). Almost 100% of people migrating with MinIO habits will crash here once.
The 403 is the most misleading: signature verification passes, but it fails at the policy layer. People troubleshooting always think the key is mistyped, but in reality, the old Service Account and its bound bucket policies were left behind in the old system.
Path-style is a hidden requirement for direct IP connections: the AWS SDK defaults to virtual-hosted-style addressing, appending the bucket name to the domain. An address like public.ServerIP directly fails DNS resolution. The old MinIO Java SDK defaulted to path-style, so it was seamless. After switching to the AWS SDK, pathStyleAccessEnabled(true) is a mandatory setting—already included in the assembly code in Section 4.1.
The last item is the trigger for this article: using the new AWS SDK to upload to an old MinIO version reports XAmzContentSHA256Mismatch. This is not a configuration error—the new SDK defaults to streaming chunked signatures, which the old MinIO cannot parse and will never fix. When you encounter it, stop troubleshooting the configuration; this is the first stone that MinIO's end-of-life has thrown at you.
A note on the new checksum pitfall: Starting in 2025, the AWS SDK enables new checksums by default. Some S3-compatible services may also report similar errors. The fallback switch is the environment variable AWS_REQUEST_CHECKSUM_CALCULATION=when_required.
VI. Conclusion
Choosing object storage is not a one-time deal; even the most mainstream solution can stop being maintained. What's truly valuable in this migration is not RustFS, but the S3 protocol abstraction—switching the server side is a configuration-level change; unifying the client to the AWS SDK, the de facto standard, means you won't even need to choose an SDK again in the future.
Future scalability:
- RustFS distributed deployment: Multi-node erasure coding for an extra level of data reliability.
- Integrate monitoring and alerting: Observability for bucket capacity and request volume.
Related Open-Source Projects:
| Project | Introduction | Source Code |
|---|---|---|
| youlai-boot | Spring Boot 4 permission management backend, the project where this article's file storage resides | Gitee · GitHub · AtomGit |
| vue3-element-admin | Companion Vue3 frontend | Gitee · GitHub · AtomGit |
Online Experience: vue.youlai.tech (PC) · app.youlai.tech (Mobile)
If your MinIO is still running in a degraded state, don't wait for the next CVE—the switching cost for S3-compatible alternatives is much lower than you think.