, 12 Min. Lesezeit, von

RustFS statt MinIO: per Docker Compose aufsetzen, Buckets mit rclone umziehen

Docker Hub liefert minio/minio nicht mehr aus. So setze ich RustFS 1.0.4 per Docker Compose auf, ziehe die Buckets mit rclone um und prüfe die Kopie vor dem Umschalten.

Seit September 2026 endet docker run minio/minio auf einem Host ohne zwischengespeichertes Image so, hier zitiert aus Issue #2255 von ODK Central:

docker: Error response from daemon: pull access denied for minio/minio, repository does not exist or may require 'docker login': denied: requested access to the resource is denied

Ein Login hilft nicht: Docker Hub liefert das Repository nicht mehr aus. Ich setze deshalb RustFS 1.0.4 per Docker Compose neben MinIO, kopiere die Buckets mit rclone über die S3-API, prüfe die Kopie und stelle restic um.

Was hinter „pull access denied“ bei minio/minio steckt

Nachgemessen am 29. Oktober 2026

Hinter „denied“ steht laut Moby-Quellcode eine 401-Antwort der Registry mit error="insufficient_scope", deshalb zeigt das Skript unten 401. minio/minio und minio/mc verschwanden laut Berichten betroffener Projekte wie Documenso am 11. September 2026 von Docker Hub; in der README und auf der Release-Seite von minio/minio fand ich am 29. Oktober 2026 keine Ankündigung von MinIO. Ob ein Image noch existiert, prüfe ich ohne docker pull direkt an der Registry:

#!/usr/bin/env bash
# regcheck.sh IMAGE [TAG]: HTTP-Status des Manifests, ohne docker pull
img="$1"; tag="${2:-latest}"
token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${img}:pull" \
  | sed -E 's/.*"token":"([^"]+)".*/\1/')
curl -sI -o /dev/null -w '%{http_code}\n' \
  -H "Authorization: Bearer ${token}" \
  -H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json' \
  "https://registry-1.docker.io/v2/${img}/manifests/${tag}"
$ ./regcheck.sh minio/minio latest
401
$ ./regcheck.sh minio/mc latest
401
$ ./regcheck.sh beispiel/gibt-es-nicht latest
401
$ ./regcheck.sh rustfs/rustfs 1.0.4
200
$ ./regcheck.sh rustfs/rustfs v1.0.4
404

minio/minio bekommt dieselbe Antwort wie ein erfundenes Repository: Das anonyme Token enthält für beide keine Pull-Berechtigung (Feld access leer). Ein 404 heißt dagegen: Repository vorhanden, Tag falsch. dl.min.io beantwortet Downloads der Community-Binaries mit 410.

Warum Weiterbetrieb keine Option ist

Seit dem 15. Oktober 2025 erscheinen neue Releases der Community Edition nur noch als Quellcode; schon das letzte Release mit dem Fix für die Rechteausweitung CVE-2025-62506 (CVSS 8,1) verweist Container-Nutzer auf den Eigenbau. Seit dem 12. Februar 2026 steht in der README „THIS REPOSITORY IS NO LONGER MAINTAINED.“, seit dem 25. April 2026 ist das Repository archiviert. Sicherheitsupdates gibt es laut dl.min.io keine mehr, und das empfohlene AIStor braucht laut seiner Container-Installationsdoku eine Lizenz, auch in der Free-Tier-Variante für eine einzelne Maschine.

Wo MinIO noch läuft, liegt das Image ohne Spiegel in einer eigenen Registry nur im lokalen Cache, und docker image prune -a löscht laut Docker-Doku alle Images ohne zugehörigen Container, nach einem docker compose down also auch dieses. Fällt der Speicher dann aus, schätzt der Ausfallkosten-Rechner die Kosten. Vor jedem Umbau sichere ich das Image:

docker image ls minio/minio
docker save -o minio-image.tar minio/minio:<TAG>
# Notfall auf einem neuen Host:
# docker load -i minio-image.tar

RustFS 1.0.4 neben MinIO starten

Compose-Dienst mit Pflicht-Zugangsdaten

RustFS ist ein S3-kompatibler Objektspeicher in Rust; 1.0.0 erschien am 16. September 2026, aktuell ist 1.0.4 vom 25. Oktober 2026. Ich ergänze den Dienst in der compose.yaml des MinIO-Projekts, damit beide im selben Netz hängen:

services:
  # minio: bleibt unveraendert, bis der Umzug geprueft ist
  rustfs:
    image: rustfs/rustfs:1.0.4
    restart: unless-stopped
    security_opt:
      - "no-new-privileges:true"
    # erst aktivieren, wenn MinIO die Ports 9000/9001 freigegeben hat
    # ports:
    #   - "127.0.0.1:9000:9000"   # S3-API, von aussen ueber Reverse Proxy
    #   - "127.0.0.1:9001:9001"   # Konsole
    environment:
      RUSTFS_ACCESS_KEY: ${RUSTFS_ACCESS_KEY:?in .env setzen}
      RUSTFS_SECRET_KEY: ${RUSTFS_SECRET_KEY:?in .env setzen}
      RUSTFS_CHECK_UPDATE: "false"
    volumes:
      - rustfs-data:/data
      - rustfs-logs:/logs
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1:9000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

# mit einem vorhandenen volumes-Block zusammenfuehren
volumes:
  rustfs-data:
  rustfs-logs:

Die Zugangsdaten stehen in der .env neben der compose.yaml (chmod 600):

RUSTFS_ACCESS_KEY=<EIGENER_ACCESS_KEY>
RUSTFS_SECRET_KEY=<EIGENES_SECRET>
docker compose up -d rustfs
docker compose ps rustfs

${VAR:?...} ist Absicht: Ohne Zugangsdaten startet der Entrypoint mit rustfsadmin/rustfsadmin und warnt nur im Log. RUSTFS_CHECK_UPDATE (ab Werk true) fragt bei jedem Start version.rustfs.com ab; von neuen Tags erfahre ich über Diun statt Watchtower. Einen HEALTHCHECK bringt das Image nicht mit; der Test oben nutzt den unauthentifizierten Endpunkt /health, das mitgelieferte curl und die Zeitwerte aus der Beispieldatei des Projekts, mehr im Beitrag zu Docker HEALTHCHECK und restart_policy.

UID 10001 und der Rechtefehler bei Bind Mounts

Gehört ein Host-Verzeichnis statt eines Named Volumes nicht der UID 10001, bricht der Start mit diesem Fehler ab (RustFS-Issue #2396):

Server encountered an error and is shutting down: Io error: Permission denied (os error 13)

Der Container läuft als Benutzer rustfs mit UID und GID 10001, und der Entrypoint lässt bestehende Mounts unverändert. Abhilfe vor dem ersten Start:

mkdir -p ./data ./logs
sudo chown -R 10001:10001 ./data ./logs
# danach in compose.yaml: ./data:/data und ./logs:/logs
# Alternative laut README: user: "<host-uid>:<host-gid>" im Dienst

Die Named Volumes im Beispiel hängen an /data und /logs, die im Image schon UID 10001 gehören. Die Doku-Beispiele setzen dagegen RUSTFS_OBS_LOG_DIRECTORY=/var/log/rustfs/, im docker run-Beispiel ohne Mount, woran Issue #6844 mit „Permission denied“ scheiterte. Ich lasse die Variable auf dem Image-Default /logs.

Umzug über die S3-API statt über das Dateisystem

Zwei Abkürzungen, die ich nicht nehme

Naheliegend wäre, das MinIO-Volume direkt in RustFS einzuhängen. RustFS führt seinen Beitrag dazu vom März 2026 inzwischen als historisches Beispiel und verweist auf seinen im Oktober 2026 überarbeiteten Migrationsleitfaden: sichern, Schreibzugriffe stoppen, über S3 kopieren, prüfen, MinIO als Rückfallebene behalten. Dazu kommt:

  • Die README von 1.0.4 stuft die Kompatibilität mit dem MinIO-Plattenformat als Preview ein. Laut Interop-Doku ist der Weg eine Einbahnstraße: Was RustFS geschrieben hat, liest MinIO nicht mehr.
  • Von MinIO verschlüsselte Objekte liest kein veröffentlichtes RustFS-Image, bei Schlüsseln aus KES, einem KMS-Plugin oder MinKMS auch kein Eigenbau mit rio-v2.

Die zweite Abkürzung, die eingebaute On-Demand-Migration, holt Objekte erst beim Lesen aus MinIO. MinIO bleibt damit Abhängigkeit, bis ein Backfill-Job den Rest geholt hat, und Listings zeigen ab Werk nur lokale Objekte, weil list_through aus ist. Mit rclone bleibt MinIO unangetastet als Rückfallebene.

rclone statt mc, mit zwei Remotes

mc mirror fällt aus: minio/mc fehlt auf Docker Hub ebenfalls, das Repository ist seit dem 14. Juli 2026 archiviert. Ich nehme rclone 1.75.2 vom 9. Oktober 2026. Die Remotes definiere ich mit den Root-Zugangsdaten beider Seiten nach dem Schema RCLONE_CONFIG_<NAME>_<OPTION>:

# rclone.env (chmod 600), Hostnamen = Compose-Dienstnamen
# Remote-Namen in Variablen: nur Grossbuchstaben, Ziffern und _
RCLONE_CONFIG_MINIO_TYPE=s3
RCLONE_CONFIG_MINIO_PROVIDER=Minio
RCLONE_CONFIG_MINIO_ACCESS_KEY_ID=<MINIO_KEY>
RCLONE_CONFIG_MINIO_SECRET_ACCESS_KEY=<MINIO_SECRET>
RCLONE_CONFIG_MINIO_REGION=us-east-1
RCLONE_CONFIG_MINIO_ENDPOINT=http://minio:9000
RCLONE_CONFIG_RUSTFS_TYPE=s3
RCLONE_CONFIG_RUSTFS_PROVIDER=Other
RCLONE_CONFIG_RUSTFS_ACCESS_KEY_ID=<RUSTFS_KEY>
RCLONE_CONFIG_RUSTFS_SECRET_ACCESS_KEY=<RUSTFS_SECRET>
RCLONE_CONFIG_RUSTFS_REGION=us-east-1
RCLONE_CONFIG_RUSTFS_ENDPOINT=http://rustfs:9000
RCLONE_CONFIG_RUSTFS_FORCE_PATH_STYLE=true

Die RustFS-Doku empfiehlt Other mit force_path_style = true, weil RustFS ohne RUSTFS_SERVER_DOMAINS nur Path-Style versteht.

Kopieren, Schreibstopp, Abgleich

Compose benennt das Netz <Projektname>_default, das Projekt im Beispiel liegt im Ordner minio; über /work landen die Listen aus rclone check auf dem Host. Den ersten Durchlauf wiederhole ich für jeden Bucket, hier am Beispiel uploads, und stoppe Schreibzugriffe erst, wenn alle einmal kopiert sind:

rcl() {
  docker run --rm --network minio_default --env-file ./rclone.env -v "$PWD:/work" \
    rclone/rclone:1.75.2 "$@"
}
rcl lsd minio:
rcl mkdir rustfs:uploads
# Bucket mit Object Lock (etwa fuer restic) stattdessen so anlegen:
#   rcl mkdir rustfs:<BUCKET> --s3-bucket-object-lock-enabled
# Versionierung einschalten, wo MinIO sie hatte:
#   rcl backend versioning rustfs:<BUCKET> Enabled

# 1. Erster Durchlauf, MinIO bleibt online (copy loescht nie)
rcl copy minio:uploads rustfs:uploads --checksum --metadata -v
# Object-Lock-Buckets: Modus und Dauer der Standardaufbewahrung
# des MinIO-Buckets explizit setzen, bei copy UND sync (siehe Text):
#   --s3-object-lock-mode GOVERNANCE --s3-object-lock-retain-until-date 365d
#   bei Objekten mit Legal Hold: --s3-object-lock-legal-hold-status copy
#   sync und Nachkopie unten dort zusaetzlich mit --s3-bypass-governance-retention

# 2. Schreibende Dienste stoppen, letzte Sicherung abwarten,
#    dann Deltas inklusive Loeschungen
docker compose stop <ANWENDUNG>
sudo systemctl stop restic-backup.timer
# letzte Sicherung, kehrt erst nach ihrem Ende zurueck
sudo systemctl start restic-backup.service
# sync und check unten fuer jeden Bucket, auch restic
rcl sync minio:uploads rustfs:uploads --checksum --metadata --dry-run
rcl sync minio:uploads rustfs:uploads --checksum --metadata -v

copy löscht nie und darf laufen, solange MinIO noch Schreibzugriffe bekommt. systemctl start wartet ohne --no-block, bis der Start abgeschlossen ist, und ein oneshot-Dienst gilt laut systemd-Doku erst als gestartet, wenn sein Prozess beendet ist; der Befehl kehrt also erst nach der letzten Sicherung zurück. sync und die Prüfung laufen für jeden Bucket, auch für restic, sonst fehlt genau diese Sicherung. Löschungen gleicht erst sync nach dem Schreibstopp ab, laut rclone-Doku vorher mit --dry-run. --metadata überträgt Nutzer-Metadaten und Header wie Cache-Control, aber keine Aufbewahrungsfrist.

In Object-Lock-Buckets setze ich --s3-object-lock-retain-until-date nicht auf copy: RustFS 1.0.4 lehnt laut Quellcode auch ein PutObject mit einem Datum, das nicht in der Zukunft liegt, mit InvalidArgument ab („The retain until date must be in the future“), eine in MinIO abgelaufene Frist ließe den Upload scheitern. Ich setze Modus und Dauer explizit, mindestens so streng wie das strengste Objekt im Bucket, das rcl lsjson -M -R minio:<BUCKET> mit object-lock-mode und object-lock-retain-until-date zeigt; die Frist beginnt dann am Kopiertag neu. Objekte im Modus GOVERNANCE löschen oder überschreiben sync und die Nachkopie danach laut rclone-Doku nur mit --s3-bypass-governance-retention.

Prüfen, umschalten, aufräumen

rclone check und die Multipart-Falle

rcl check minio:uploads rustfs:uploads
# meldet die Zusammenfassung "N hashes could not be checked": byteweise pruefen,
# Ergebnis je Pfad nach check.txt schreiben
rcl check minio:uploads rustfs:uploads --download --combined /work/check.txt
# verschiedene (*), im Ziel fehlende (+) und fehlerhafte (!) Objekte erneut
# kopieren (ein weiterer sync --checksum hielte gleich grosse fuer identisch)
grep -E '^[*+!] ' check.txt | sed 's/^. //' > redo.txt
rcl copy minio:uploads rustfs:uploads --files-from-raw /work/redo.txt --ignore-times --metadata -v
# erst umschalten, wenn check "exit=0" meldet und grep nichts ausgibt
rcl check minio:uploads rustfs:uploads --download --combined /work/check.txt; echo "exit=$?"
grep -v '^= ' check.txt

rclone check vergleicht Größe und MD5. Bei Multipart-Objekten ist der ETag aber kein MD5; rclone kennt den Hash dann nur aus X-Amz-Meta-Md5chksum, das es bei eigenen Uploads ergänzt, sofern die Quelle einen MD5 liefert. Hat ein anderer Client das Objekt in MinIO hochgeladen, fehlt er auf beiden Seiten, check vergleicht nur die Größe und meldet „N hashes could not be checked“. Ist N größer null, vergleiche ich mit --download Byte für Byte. --combined markiert jeden Pfad: = gleich, * verschieden, + fehlt im Ziel, - nur im Ziel, ! Lese- oder Hashfehler. Die Objekte mit *, + und ! kopiere ich mit --ignore-times erneut, denn ein weiterer sync --checksum hielte gleich große Objekte ohne vergleichbaren Hash laut rclone-Quellcode für identisch. --files-from-raw übernimmt die Namen unverändert, --files-from entfernt Leerzeichen am Zeilenrand und überspringt Zeilen mit # oder ; am Anfang. Umgeschaltet wird erst, wenn der zweite check mit Exit-Code 0 endet und nur =-Zeilen liefert, denn Unterschiede und Fehler meldet rclone laut Doku und Quellcode mit einem Exit-Code ungleich 0.

restic umstellen, MinIO abbauen

Danach stoppe ich MinIO und gebe die Ports an RustFS:

docker compose stop minio
# compose.yaml: ports bei minio auskommentieren, bei rustfs einkommentieren;
# depends_on: minio in den Anwendungen auf rustfs aendern
docker compose up -d rustfs

Die Konsole auf Port 9001 erreiche ich per SSH-Tunnel (ssh -L 9001:127.0.0.1:9001 <USER>@<HOST>), weil sie nur auf 127.0.0.1 lauscht. Dort lege ich Benutzer, Policies und Bucket-Einstellungen neu an, auch einen eigenen Benutzer für restic, denn rclone überträgt nur Objekte in ihrer aktuellen Version. Im EnvironmentFile aus dem Beitrag zu verschlüsselten Backups mit restic nach S3 ändere ich nur Endpoint und Schlüssel:

# /etc/restic/env
RESTIC_REPOSITORY=s3:http://127.0.0.1:9000/restic
AWS_ACCESS_KEY_ID=<RESTIC_KEY>
AWS_SECRET_ACCESS_KEY=<RESTIC_SECRET>
RESTIC_PASSWORD=<UNVERAENDERT>
# als root
set -a; . /etc/restic/env; set +a
restic snapshots
restic check --read-data-subset=10%
systemctl start restic-backup.timer

# Anwendungen: Endpoint http://rustfs:9000 und neue Schluessel, dann
docker compose up -d <ANWENDUNG>

Bis zur ersten Sicherung über RustFS und einem erfolgreichen Test-Restore bleibt der Rückweg offen. docker compose start minio reicht dafür nicht, denn der vorhandene Container hat noch die Bindungen an 9000 und 9001; docker compose up -d minio legt ihn nach der geänderten compose.yaml ohne Ports neu an. Dann läuft der Hinweg rückwärts: Anwendungen und Backup-Timer stoppen, die letzte Sicherung mit systemctl start abwarten, für jeden Bucket rcl sync rustfs:uploads minio:uploads --checksum --metadata erst mit --dry-run, dann scharf, wobei sync in MinIO löscht, was auf RustFS gelöscht wurde. Danach folgen check --download, Nachkopie und zweiter check mit vertauschten Remotes, denn auch hier übersieht --checksum geänderte Multipart-Objekte gleicher Größe. Erst wenn dieser check sauber endet, tausche ich Ports, Endpoint und Schlüssel zurück. Ist der Umzug bestätigt, entferne ich den Dienst minio, räume mit docker compose up -d --remove-orphans auf und lösche dann minio-image.tar, die rclone.env mit beiden Root-Schlüsseln und das Volume.

RustFS, Garage und SeaweedFS im Vergleich

Eckdaten am 29. Oktober 2026

Die Größen stammen aus der Docker-Hub-API (images[].size, linux/amd64, komprimiert), auf ganze Megabyte gerundet.

RustFS Garage SeaweedFS MinIO Community
Version 1.0.4 (25.10.2026) v2.5.0 (24.10.2026) 4.51 (25.10.2026) RELEASE.2025-10-15T17-29-55Z, nur Quellcode
Lizenz Apache 2.0 AGPL 3.0 Apache 2.0 AGPL 3.0
Docker-Tag rustfs/rustfs:1.0.4 dxflrs/garage:v2.5.0 chrislusf/seaweedfs:4.51 kein Image mehr
Image amd64, komprimiert 115 MB 28 MB 197 MB entfällt

Wann Garage oder SeaweedFS besser passt

Garage hat das kleinste Image, kennt laut eigener Kompatibilitätsliste aber weder Versionierung noch Object Lock oder Bucket-Policies: gut für Upload-Buckets, nichts für unveränderliche Backups. SeaweedFS passt, wenn neben S3 ein FUSE-Mount gefragt ist, den seine Filer-Server mitbringen. Nach meiner Einschätzung kommt RustFS MinIO mit Versionierung, Object Lock und IAM am nächsten, seine Kompatibilitätsmatrix mit Stand 9. August 2026 deckt aber nur eine getestete Teilmenge der S3-API ab, ohne ACL-Autorisierung.

Fragen zum Umstieg von MinIO

Kann ich auf quay.io oder ein selbst gebautes Image ausweichen?

quay.io/minio/minio liefert anonym ebenfalls 401. Selbst bauen geht, Sicherheitsupdates gibt es aber keine mehr.

Was muss ich hinter einem Reverse Proxy beachten?

Ohne RUSTFS_SERVER_DOMAINS brauchen Clients Path-Style, und nginx muss den Host-Header so weiterreichen, wie der Client ihn signiert hat. Die nginx-Anleitung der RustFS-Doku setzt dafür proxy_set_header Host $http_host;, ein Maintainer empfahl das in Issue #1184 gegen SignatureDoesNotMatch. Mehr dazu im Beitrag zu nginx als Reverse Proxy im Container.

Warum 1.0.4 und nicht latest?

latest zeigt am 29. Oktober 2026 auf denselben Digest wie 1.0.4, mit festem Tag entscheide ich selbst über Updates. 1.0.1 und älter scheiden aus: Fünf Sicherheitshinweise von RustFS betreffen 1.0.1 und sind ab 1.0.2 behoben, darunter einer mit CVSS 8,1, bei dem WebDAV-, FTP- und SFTP-Zugänge Bucket-Policies umgehen. Für 1.0.2 bis 1.0.4 listet RustFS am 29. Oktober keinen offenen Hinweis.

Offizielle Quellen für den Umzug

RustFS und rclone

Alle Variablen und Optionen stehen in der Docker-Installation von RustFS, den Umgebungsvariablen, der S3-Kompatibilitätsmatrix, dem S3-Backend von rclone und bei rclone check.

MinIO-Status und Alternativen

Den Wartungshinweis zeigt das archivierte MinIO-Repository, die Alternativen beschreiben die S3-Kompatibilitätsliste von Garage und das SeaweedFS-Repository. Wenn Sie den Umzug nicht allein durchführen wollen, übernehme ich ihn im Rahmen meines IT-Supports.

Hinweis: Die Beiträge dieses Blogs werden unter Einsatz von KI erstellt und vor der Veröffentlichung redaktionell geprüft. Die redaktionelle Verantwortung trägt Emre Yurtbay (siehe Impressum).

Lassen Sie uns sprechen

Im kostenlosen Erstgespräch schildern Sie mir Ihre Situation. Ich gebe Ihnen eine erste Einschätzung, wie ich Sie unterstützen kann.

Kostenloses Erstgespräch anfragen