MinIO war jahrelang die Standardantwort auf die Frage nach S3-kompatiblem Object Storage im eigenen Cluster. Seit Ende 2025 gilt das nur noch eingeschränkt: Die Community-Edition ist im Dezember 2025 in den Maintenance-Modus gewechselt, das öffentliche Repository wurde Anfang 2026 archiviert. Neue Funktionen entstehen ausschließlich in der kommerziellen Variante. Für viele Self-Hosting-Setups ist das der Anlass, sich nach einer dauerhaft quelloffenen Alternative umzusehen. SeaweedFS ist dafür ein ausgereifter Kandidat: Apache-2.0-lizenziert, aktiv weiterentwickelt und S3-kompatibel.
Dieser Beitrag zeigt anhand eines praxisnahen Ablaufs, wie SeaweedFS per Helm im HA-Setup auf Kubernetes installiert und bestehende Buckets anschließend per rclone-Job migriert werden – bis hin zum Cutover.
SeaweedFS in Kürze
Wo MinIO ein einziger, symmetrischer Server-Prozess ist, teilt SeaweedFS die Aufgaben auf mehrere Rollen auf: der Master koordiniert den Cluster (Leader-Election per Raft), die Volume Server halten die eigentlichen Daten, der Filer legt den Namespace darüber, und ein zustandsloses S3-Gateway stellt die S3-API bereit. Für ein Setup, das man an SeaweedFS anbindet, spricht man am Ende nur mit dem S3-Gateway – der Rest ist Innenleben.
Zwei Dinge entscheiden über echtes HA: Der Master ist erst mit drei Replicas hochverfügbar (Raft-Quorum), und der Filer braucht einen gemeinsamen Metadaten-Store (PostgreSQL, MySQL oder Redis) – mehrere Filer mit lokalem leveldb sind kein HA.
Installation von SeaweedFS per Helm
SeaweedFS bringt ein offizielles Helm-Chart mit, das die einzelnen Rollen als getrennte StatefulSets ausrollt. Das ist genau der Modus, den wir für ein HA-Setup brauchen. Zunächst das Chart-Repository einbinden:
helm repo add seaweedfs https://seaweedfs.github.io/seaweedfs/helm helm repo update
Die eigentliche Arbeit steckt in den Values. Für ein hochverfügbares Setup bekommt jede Rolle drei Replicas und replicationPlacement legt fest, wie SeaweedFS die Daten über die Volume Server verteilt: Der Wert 001 sorgt dafür, dass jeder Chunk zusätzlich auf einem zweiten Server im selben Cluster liegt.
# seaweedfs-ha-values.yaml
global:
replicationPlacement: "001" # eine zusätzliche Kopie auf anderem Volume-Server
master:
replicas: 3 # 3 = Raft-Quorum, erst dann HA
data: { type: persistentVolumeClaim, size: 5Gi, storageClass: standard }
volume:
replicas: 3
dataDirs:
- name: data
type: persistentVolumeClaim
size: 100Gi # an Datenmenge anpassen
storageClass: standard
filer:
replicas: 3
# Für echtes HA: gemeinsamen Store (Postgres/MySQL/Redis) statt lokalem leveldb.
data: { type: persistentVolumeClaim, size: 10Gi, storageClass: standard }
s3:
enabled: true
replicas: 3
enableAuth: true
Die Größenangaben sind bewusst klein gehalten: Master und Filer speichern nur Metadaten, der Platzbedarf liegt also fast vollständig bei den Volume-Servern. Mit diesen Values lässt sich das Chart ausrollen:
helm upgrade --install seaweedfs seaweedfs/seaweedfs \ --namespace storage --create-namespace \ -f seaweedfs-ha-values.yaml
Der erste Start dauert einen Moment, weil die Master erst untereinander ein Quorum bilden müssen, bevor sich Volume Server und Filer anmelden können. Ein Blick auf die Pods zeigt, ob alle Rollen oben sind:
kubectl -n storage get pods -l app.kubernetes.io/name=seaweedfs
Sobald alles läuft, ist das S3-Gateway clusterintern unter http://seaweedfs-s3.storage.svc:8333 erreichbar – das ist die Adresse, die später sowohl der Migrations-Job als auch die Anwendung selbst verwendet.
Migration von MinIO
Der Ansatz: Beide Object Stores laufen parallel, die Daten werden in einem Wartungsfenster per rclone synchronisiert und verifiziert, dann stellt man die Anwendung um. MinIO bleibt als Fallback stehen. rclone passt hier gut, weil es beide Seiten als reines S3 spricht und mit sync idempotent arbeitet – ein abgebrochener Lauf lässt sich risikofrei wiederholen.
Vorab das Ziel-Bucket in SeaweedFS anlegen und die Anwendung read-only schalten, damit während der Migration nichts mehr nach MinIO geschrieben wird.
Das Manifest besteht aus zwei Teilen: Eine ConfigMap hält die rclone.conf mit beiden Endpunkten sowie das Migrationsskript, ein Job mountet beides und führt es aus. Beide Stores sind darin schlicht zwei S3-Remotes – der eigentliche Wechsel reduziert sich damit auf einen Kopiervorgang zwischen zwei Adressen:
# rclone-migration.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: rclone-migration-config
namespace: storage
data:
rclone.conf: |
[minio]
type = s3
provider = Minio
access_key_id = MINIO_ACCESS_KEY
secret_access_key = MINIO_SECRET_KEY
endpoint = http://minio.storage.svc:9000
region = us-east-1
[seaweedfs]
type = s3
provider = Other
access_key_id = SEAWEEDFS_ACCESS_KEY
secret_access_key = SEAWEEDFS_SECRET_KEY
endpoint = http://seaweedfs-s3.storage.svc:8333
region = us-east-1
force_path_style = true
migrate.sh: |
#!/bin/sh
set -e
BUCKET="my-app-data"
FLAGS="--transfers=16 --checkers=8 --checksum --retries=3 --stats=10s --stats-one-line"
rclone sync minio:$BUCKET seaweedfs:$BUCKET $FLAGS
rclone check minio:$BUCKET seaweedfs:$BUCKET --one-way $FLAGS
echo "Migration OK"
---
apiVersion: batch/v1
kind: Job
metadata:
name: rclone-minio-to-seaweedfs
namespace: storage
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
containers:
- name: rclone
image: rclone/rclone:latest
command: ["/bin/sh", "/scripts/migrate.sh"]
volumeMounts:
- { name: config, mountPath: /config/rclone, readOnly: true }
- { name: scripts, mountPath: /scripts, readOnly: true }
volumes:
- name: config
configMap:
name: rclone-migration-config
items: [{ key: rclone.conf, path: rclone.conf }]
- name: scripts
configMap:
name: rclone-migration-config
items: [{ key: migrate.sh, path: migrate.sh, mode: 0755 }]
Im Produktivbetrieb gehören die Access-Keys in ein Secret, nicht in die ConfigMap. Hier stehen sie zur besseren Lesbarkeit inline 😉
Ausgerollt wird der Job wie jedes andere Manifest; die Logs zeigen den Fortschritt live mit:
kubectl apply -f rclone-migration.yaml kubectl -n storage logs -f job/rclone-minio-to-seaweedfs
rclone check vergleicht Quelle und Ziel und lässt den Job fehlschlagen, falls etwas nicht übertragen wurde – die Verifikation ist also direkt eingebaut.
Cutover: Anwendungskonfiguration auf den SeaweedFS-Endpoint umstellen (endpoint: http://seaweedfs-s3.storage.svc:8333, forcePathStyle: true – SeaweedFS erwartet Path-Style), Wartungsmodus deaktivieren, Smoke-Test fahren. MinIO noch ein bis zwei Wochen als Fallback stehen lassen, dann abbauen.
Worauf ist zu achten?
- Path-Style: SeaweedFS adressiert per Path-Style. In
rclone(force_path_style = true) und in der App (forcePathStyle: true) setzen, sonst laufen Requests ins Leere. - Filer-HA: Mehrere Filer-Replicas brauchen einen gemeinsamen Metadaten-Store – sonst driften die Namespaces auseinander.
- S3-Randfälle: Multipart-Uploads und Presigned URLs verhalten sich zwischen Implementierungen leicht unterschiedlich. Vor dem produktiven Cutover im Staging testen.
Fazit
Die Migration ist kein Big-Bang, sondern ein planbarer Zwei-Schritt: HA-Setup per Helm, dann eine idempotente rclone-Migration mit eingebauter Prüfung. Weil beide Systeme reines S3 sprechen, bleibt der Eingriff auf Infrastrukturebene – die Anwendungslogik bleibt unangetastet. Und mit MinIO im Fallback verliert der Cutover seinen Schrecken.
