跪拜 Guibai
← Back to the summary

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.

Insert image description here

Highlights of this article:


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:

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:

mermaid diagram

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:

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. Using chmod 777 bypasses this but effectively opens write permissions to all users on the server. Where does 10001 come from? You can check with docker 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:

  1. 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.
  2. 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.

Insert image description here

Insert image description here

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.

Insert image description here

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. Insert image description here 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.

Insert image description here


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:


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.