16. Juni 2026 · von Andreas Lehr
SeaweedFS auf Hetzner: S3-Storage selbst betreiben – von der Platte bis zur Kundenmigration
E-Mail war das Thema der letzten beiden Artikel – heute geht es um etwas Handfesteres: Bleche, Platten und ein S3-kompatibles Object Storage, das wir selbst betreiben. Konkret um die Frage, wie man auf gewöhnlicher Hetzner-Hardware ein robustes, günstiges S3-Backend aufbaut, mit dem sich Backups, Assets und alles andere ablegen lässt, was eine S3-API spricht, ohne sich an einen Hyperscaler oder dessen Object-Storage-Preise zu binden.
Als durchgängiges Beispiel nehmen wir unsere eigene Backup-Infrastruktur, die wir Anfang des Jahres von MinIO auf SeaweedFS umgezogen haben: über 100 TB Daten, über 40 Kundenbuckets, alle mit restic befüllt.
Warum weg von MinIO – und von Hetzner Object Storage
Zwei Dinge kamen zusammen. Erstens hat MinIO seine Lizenz- und Feature-Politik in eine Richtung entwickelt, die für einen Self-Hosting-Betrieb wie unseren zunehmend unattraktiv wurde: Funktionen aus der Community-Edition wanderten in kostenpflichtige Tiers, und die Unsicherheit, was als Nächstes wegfällt, ist für ein produktives Backup-Ziel kein guter Zustand.
Zweitens war ohnehin klar: Wir wollen das Object Storage auf eigener Hardware betreiben, nicht über einen gemanagten Dienst einkaufen. Die Rechnung ist eindeutig. Hetzner Object Storage startet bei 7,72 €/TB für das erste TB, jedes weitere TB liegt bei 10,35 €/TB im Monat. 1 TB ausgehender Traffic ist inklusive, jedes weitere TB kostet 1,19 €. Was Hetzner anders als viele Anbieter macht: Requests sind kostenlos, und interner Traffic – Uploads und alles innerhalb der Hetzner-EU-Infrastruktur – wird nicht berechnet. Trotzdem landest du bei 200 TB allein für Storage bei knapp 2.100 € im Monat. Dazu kommt ein hartes Limit von 50 Millionen Objekten pro Account (Details und weitere Grenzen stehen in der Hetzner-Doku) – bei Backup-Workloads mit vielen kleinen Files schnell ein Stopper. Ein dedizierter Hetzner Storage-Server mit derselben Rohkapazität kostet einen Bruchteil davon, und die Backup-Software (restic, rclone) ist Open Source und kostenlos. Der zweite Server amortisiert sich gegenüber gemanagtem Object Storage oft schon in wenigen Monaten.
Dazu kommt ein zweiter Punkt, der uns am Ende fast wichtiger war als die Kosten: Hetzner Object Storage ist aktuell spürbar instabil. Die offizielle Statuspage zeigt regelmäßig Vorfälle rund um Object Storage – je nach Standort teils mehrfach pro Woche. Für ein produktives Backup-Ziel, das am Ende einer Recovery-Kette steht, ist das schlicht keine Option.
Genau diese Logik – kalkulierbare eigene Infrastruktur statt nutzungsbasierter Preise, dazu volle Kontrolle über die Verfügbarkeit – ist der Grund, warum SeaweedFS für uns Sinn ergibt.
Die Hardware: Storage-Server bei Hetzner
Die Server kommen aus dem Hetzner-Angebot, je nach Bedarf als Server aus der SX-Storage-Linie oder als Auktionsserver. Für reine Storage-Knoten sind die Auktionsserver besonders attraktiv: viele große SATA-HDDs, dazu ein, zwei NVMe-SSDs fürs System, für einen Bruchteil des Neupreises.
Was Hetzner zusätzlich bietet und für Storage-Knoten Sinn ergibt: ein optionaler 10G-Uplink für dedizierte Server. Standardmäßig hängt jeder Hetzner-Server an 1 GBit/s — bei Initial-Migrationen über 100 TB, Restore-Tests oder Geo-Sync zwischen zwei Storage-Servern ist 10 GBit/s der Unterschied zwischen „läuft über Nacht" und „läuft über das Wochenende".
Die Platten laufen im Software-RAID 6 (mdadm). RAID 6 opfert zwei Platten für Parität und verträgt damit den gleichzeitigen Ausfall von zwei Festplatten. Bei großen Arrays mit langen Rebuild-Zeiten ist das die richtige Wahl. Ein Rechenbeispiel mit einem gut bestückten Server:
14 × 22 TB = 308 TB roh
− 2 Platten Parität (RAID 6)
= 12 × 22 TB = 264 TB nutzbar
≈ 250 TB nach Filesystem-Overhead (XFS)
Als Dateisystem kommt XFS zum Einsatz:
mkfs.xfs -f -L seaweed-data /dev/md0
Warum XFS und nicht ext4? Weil SeaweedFS intern keine Millionen kleiner Dateien anlegt, sondern wenige große Volume-Dateien (standardmäßig bis 30 GB pro Volume). XFS handhabt genau dieses Muster (große, sequenziell wachsende Dateien auf großen Volumes) hervorragend.
SeaweedFS verstehen: Master, Volume, Filer
Bevor wir installieren, kurz die Architektur, denn SeaweedFS besteht aus mehreren Komponenten, die man kennen sollte:
┌──────────────────────────────────────┐
│ nginx (443) │
│ Reverse Proxy / TLS │
└───────────────────┬──────────────────┘
│
┌───────────────────▼──────────────────┐
│ Filer + S3-Gateway │ Port 8333 (S3)
│ S3-API, Buckets, Credentials │ Port 8888 (Filer)
└───────────────────┬──────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
┌──▼──────────┐ ┌─────────▼─────────┐
│ Master │ │ Volume Server │
│ Topologie / │ │ Daten auf RAID6 │
│ Raft │ │ (XFS / md0) │
└─────────────┘ └───────────────────┘
Der Master verwaltet die Topologie und die Volume-Zuordnung (über Raft, später auch HA-fähig mit mehreren Mastern).
Der Volume Server schreibt die eigentlichen Daten auf das RAID-Array.
Der Filer liefert das S3-Gateway auf Port 8333 – das ist die Schnittstelle, mit der restic, rclone und Co. reden.
Das Schöne: SeaweedFS ist ein einziges Go-Binary (weed), das je nach Aufruf die jeweilige Rolle übernimmt. Keine Abhängigkeitshölle, kein Container-Stack – ein Binary, ein paar systemd-Units, fertig.
Installation und Betrieb mit Ansible
Für die Community gibt es zwar ein paar SeaweedFS-Ansible-Rollen, aber die meisten hängen auf alten 2.x-Versionen fest. Da SeaweedFS so simpel aufgebaut ist, haben wir stattdessen eine eigene, schlanke Rolle gebaut. Sie fügt sich sauber in unsere bestehende Ansible-Struktur ein und wird über einen App-Type vom Base-Host referenziert.
Wir haben die Rolle unter MIT-Lizenz auf GitHub veröffentlicht – wer SeaweedFS auf eigener Hardware betreiben möchte, kann sie als Startpunkt nehmen oder forken: github.com/we-manage/we-manage.seaweedfs.
Die Rolle ist nach dem üblichen Schema aufgebaut:
roles/seaweedfs/
├── defaults/main.yml # Version, Ports, Pfade, Bucket-Definitionen
├── tasks/
│ ├── main.yml # Orchestrierung
│ └── install.yml # Binary-Download, Mount, systemd
├── templates/
│ ├── master.service.j2
│ ├── volume.service.j2
│ ├── filer.service.j2
│ ├── s3.json.j2 # Identities / Per-Bucket-Credentials
│ └── nginx.conf.j2
└── handlers/main.yml
Die Installation des Binaries ist trivial und über die Rolle versionsgepinnt, so wissen wir immer genau, welche Version auf welchem Server läuft:
wget https://github.com/seaweedfs/seaweedfs/releases/download/4.34/linux_amd64.tar.gz
tar xzf linux_amd64.tar.gz
install -m 0755 weed /usr/local/bin/weed
Die drei Dienste laufen als getrennte systemd-Units. Der entscheidende Teil ist das S3-Gateway mit Per-Bucket-Credentials. SeaweedFS verwaltet Zugangsdaten über eine Identities-JSON – jeder Kunde bekommt seinen eigenen Access-/Secret-Key und sieht nur seinen eigenen Bucket. Vereinfacht sieht so ein Eintrag so aus:
{
"identities": [
{
"name": "kunde-beispiel",
"credentials": [
{ "accessKey": "...", "secretKey": "..." }
],
"actions": [ "Read:kunde-beispiel", "Write:kunde-beispiel", "List:kunde-beispiel" ]
}
]
}
Die Keys selbst liegen verschlüsselt in den Ansible group_vars – das Template rendert daraus die fertige S3-Konfiguration. Neue Kunden bedeuten damit lediglich einen Eintrag in den Variablen und einen Playbook-Lauf.
Davor hängt nginx als Reverse Proxy, der TLS terminiert und den S3-Endpoint nach außen gibt. Zwei Einstellungen sind dabei wichtig, sonst macht restic mit großen Objekten Ärger:
server {
listen 443 ssl;
server_name storage.example.com;
client_max_body_size 0; # keine Größenbegrenzung für große Uploads
proxy_buffering off; # Streaming statt Zwischenpufferung
location / {
proxy_pass http://127.0.0.1:8333;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 600;
proxy_send_timeout 600;
}
}
client_max_body_size 0 hebt die Upload-Größenbegrenzung auf, proxy_buffering off sorgt dafür, dass große Objekte gestreamt statt komplett zwischengepuffert werden. Die großzügigen Timeouts geben restic-Operationen genug Luft.
Die Migration: phasenweise von MinIO auf SeaweedFS
Das war der Teil, vor dem wir am meisten Respekt hatten – und der dann erstaunlich glatt lief. Wir sind bewusst phasenweise vorgegangen statt mit Big-Bang-Cutover: erst die kleinen Kunden auf den neuen Server, parallel zum laufenden MinIO. So konnten wir die SeaweedFS-Plattform unter realer Last verifizieren, ohne die großen Buckets zu riskieren. Schrittweise zogen wir dann die größeren Kunden nach. Eine Zeit lang lief beides parallel – die Realbedingungen waren gleichzeitig der Test.
Der Ablauf war:
Neuen Storage-Server aufsetzen und SeaweedFS per Ansible installieren – parallel zum laufenden MinIO, ohne dort etwas anzufassen.
Alle Buckets samt Credentials anlegen. Dafür gibt es ein eigenes Playbook, das über die Kundenliste in den
group_varsiteriert und in einem Lauf alle Buckets und zugehörigen Zugangsdaten erstellt – über 40 Kundenbuckets in wenigen Minuten.Daten übertragen und anschließend die Endpoints umschwenken. Erst als alles sauber gespiegelt war und die ersten Backups gegen SeaweedFS liefen, wurde MinIO abgeschaltet.
Pro Kunde war das technisch ein dreistufiger Lauf:
rclone copy minio:kunde seaweed:kunde --transfers 32 --checkers 32
rclone check minio:kunde seaweed:kunde --checkers 32
ansible-playbook base-host.yml -e env=kunde_all -t restic
rclone check ist dabei der entscheidende Schritt: byteweiser Vergleich vor dem Endpoint-Switch. Wer über 100 TB ohne Verify kopiert, hat keinen Backup-Stand, sondern ein Versprechen. Der Endpoint-Switch selbst passiert per Ansible-Re-Deploy mit Tag restic – das aktualisiert die restic-Konfiguration auf den Backup-Clients und richtet sie auf den neuen Server. Ab dem nächsten Backup-Lauf schreiben sie nach SeaweedFS.
restic gegen SeaweedFS
Die Kunden-Backups laufen mit restic, das die S3-API von SeaweedFS direkt anspricht. restic und S3 sind ein gut eingespieltes Gespann: Deduplizierung, Verschlüsselung und Snapshot-Verwaltung passieren clientseitig, das Object Storage muss nur stumpf Objekte halten. Für SeaweedFS heißt das wenig Konfigurationsaufwand. Es ist schlicht ein S3-Ziel.
Ein Punkt, auf den man bei großen restic-Repositories achten sollte, sind die Prune- und Retention-Läufe: Sie löschen alte Pack-Files und verändern das Repository spürbar. Das ist für die Georedundanz relevant, denn der Spiegel muss diese Löschungen mit nachvollziehen. Dazu gleich mehr.
Georedundanz: ein zweiter Standort als Replica
Ein Backup an einem einzigen Standort ist kein vollständiges Disaster-Recovery-Konzept. Deshalb gibt es einen zweiten Storage-Server an einem anderen Hetzner-Standort, geografisch getrennt, damit ein Rechenzentrumsausfall nicht beide Kopien gleichzeitig trifft. Nennen wir sie Server 1 (Primär) und Server 2 (Replica).
SeaweedFS bringt für Replikation eigene Mechanismen mit, etwa cluster-interne Volume-Replikation oder filer.sync für Cross-Datacenter-Setups. Für unseren Fall (ein reiner Disaster-Recovery-Spiegel, kein synchroner Live-Cluster) setzen wir bewusst auf rclone sync als geplanten Lauf. Die Gründe:
Es ist ein kontrollierter, planbarer Lauf statt einer permanenten Verbindung zwischen den Standorten.
Es spiegelt bucketweise und nachvollziehbar, inklusive der Löschungen, die restics Prune-Läufe verursachen.
Es ist simpel im Betrieb: eine rclone-Config, ein systemd-Timer, fertig.
Der entscheidende Kniff: Der Sync läuft tagsüber, bewusst zeitversetzt zu den nächtlichen Backup-Läufen. Dadurch entsteht ein gewollter zeitlicher Abstand zwischen Original und Replica. Sollte ein nächtlicher Lauf Daten beschädigen oder versehentlich Falsches schreiben, ist dieser Stand nicht sofort auf den zweiten Server gespiegelt. Es bleibt ein Zeitfenster, um zu reagieren, bevor der Replica nachzieht. Ein synchroner Live-Mirror hätte den Fehler dagegen umgehend mitkopiert.
Die rclone-Konfiguration liegt unter /etc/rclone/rclone.conf und definiert beide Standorte als S3-Remotes:
[server1]
type = s3
provider = Other
endpoint = https://storage-1.example.com
access_key_id = ...
secret_access_key = ...
[server2]
type = s3
provider = Other
endpoint = https://storage-2.example.com
access_key_id = ...
secret_access_key = ...
Der eigentliche Abgleich ist ein Einzeiler, der per Timer läuft:
rclone sync server1: server2: --transfers 4 --checkers 8
sync (nicht copy) macht Server 2 zum exakten Spiegel von Server 1 – was auf der Quelle gelöscht wurde, verschwindet auch auf dem Ziel. So bleibt der Replica konsistent mit dem Hauptbestand, inklusive Retention. Bei den Transfer-Parametern sind wir bewusst zurückhaltend: Vier parallele Transfers reichen für einen einzelnen Storage-Server völlig und vermeiden, dass der Sync das System überlastet.
Hardware überwachen: SMART, RAID und Temperaturen
Wer zweistellige Plattenzahlen in einem Server betreibt, muss sie überwachen – nicht aus Prinzip, sondern weil bei vielen HDDs statistisch regelmäßig eine ausfällt. Genau dafür haben wir ein universelles Hardware-Monitoring im Einsatz, das wir über die Zeit gebaut und verfeinert haben: ein Python-Skript, das Sensorwerte erfasst und sie über Telegraf → VictoriaMetrics → Grafana sichtbar macht.
Bei Software-RAID lesen wir die Disk-Temperaturen direkt per smartctl von den einzelnen Platten aus, die RAID-Mitglieder sind ja gewöhnliche Block-Devices. Der RAID-Zustand selbst kommt aus /proc/mdstat bzw. mdadm. Besonders im Blick behalten wir die kritischen SMART-Attribute, die einen baldigen Plattenausfall ankündigen:
for disk in /dev/sd?; do
echo "=== $disk ==="
smartctl -a "$disk" | grep -iE "reallocated|pending|uncorrectable"
done
Reallocated Sectors, Pending Sectors und Uncorrectable Errors sind die Frühwarnindikatoren: Steigen die Werte, wird die Platte getauscht, bevor sie ganz aufgibt. Dazu kommen Health-Checks in Icinga2, die im Ernstfall sofort Alarm schlagen. So wissen wir von einem Plattenproblem, lange bevor das RAID in einen kritischen Zustand gerät.
Genau diese Überwachung gilt natürlich auch für den zweiten Standort – beide Server hängen am selben Monitoring-Stack.
Fazit
SeaweedFS auf eigener Hetzner-Hardware ist kein Hexenwerk. Im Kern sind es ein Go-Binary, ein RAID-6-Array mit XFS und eine saubere Ansible-Rolle, die den Betrieb reproduzierbar macht. Dafür bekommt man ein vollwertiges, S3-kompatibles Object Storage mit voller Kostenkontrolle, ohne Vendor-Lock-in und ohne Lizenz-Überraschungen – auf europäischer Infrastruktur.
Der Aufwand steckt weniger in der Installation als im sauberen Drumherum: durchdachte Per-Bucket-Credentials, eine Migration ohne Client-Ausfall, ein geografisch getrennter Replica und, nicht zu unterschätzen, ein verlässliches Hardware-Monitoring. Wer das einmal sauber aufsetzt, hat eine Storage-Basis, die über Jahre trägt und mitwächst.
Falls du selbst hosten willst, dir den Betrieb von Storage-Servern, Migration und Monitoring aber nicht ans Bein binden möchtest: Genau dafür sind wir da. Wir betreiben für unsere Kunden sowohl Cloud- als auch dedizierte Hardware-Server – inklusive Setup, Migration und laufendem Monitoring. Sprich uns an.