Ein Backup des Container-Images sichert die Anwendung, nicht die Daten. Alles, was in einem Named Volume liegt, bleibt dabei außen vor: Datenbankdateien, Uploads, Zertifikate. Diese Anleitung zeigt, wie du Docker-Volumes mit Restic clientseitig verschlüsselt in ein S3-Repository sicherst, den Lauf per systemd-Timer automatisierst und den Restore nachweisbar prüfst.
Warum reicht ein Image-Backup des Containers nicht?
Ein Image-Backup mit docker commit oder docker save sichert nur die Layer des Container-Dateisystems, nicht die Named Volumes, denn Volumes liegen beim Standard-Treiber local außerhalb der Image-Layer unter /var/lib/docker/volumes/<name>/_data.
Das lässt sich in einem Schritt nachvollziehen. Der folgende Befehl zeigt für jeden Container, welche Volumes eingehängt sind:
$ docker ps --format '{{.Names}}' | xargs -I{} sh -c 'echo "== {}"; docker inspect -f "{{range .Mounts}}{{.Type}} {{.Name}} -> {{.Destination}}{{println}}{{end}}" {}'Jede Zeile mit volume ist ein Datenbestand, den ein Image-Backup nicht erfasst. Genau diese Pfade sichert Restic.
Voraussetzungen
- Ein Linux-Host mit Docker Engine 24.0 oder neuer und
docker composev2 - Restic 0.17 oder neuer (
restic version), Repository-Format Version 2 mit Kompression ist dort Standard - Ein S3-kompatibler Bucket samt Access Key und Secret Key
- root-Rechte auf dem Host, da
/var/lib/docker/volumesnur für root lesbar ist - Freier Speicherplatz für einen lokalen Testrestore, mindestens in Größe des größten Volumes
Restic sichert Dateien, nicht Blockgeräte. Ein Volume-Backup ersetzt daher kein vollständiges Server-Image. Beides ergänzt sich: automatisierte Disk-Image-Backups des gesamten Servers decken den Totalausfall der VM ab, Restic deckt granulare Wiederherstellung einzelner Dateien und Versionsstände ab.
Installiere Restic aus den Paketquellen:
$ sudo apt update && sudo apt install -y restic
$ restic version
restic 0.17.3 compiled with go1.23.4 on linux/amd64Ist die Paketversion älter als 0.17, lade das statische Binary von den offiziellen Releases und lege es nach /usr/local/bin/restic.
Wie sichert Restic ein Docker-Volume?
Restic liest entweder das Volume-Verzeichnis auf dem Host oder einen Datenstrom von stdin, zerlegt den Inhalt in variabel große Chunks, dedupliziert sie gegen das bestehende Repository und verschlüsselt jeden Blob clientseitig mit AES-256 vor dem Upload.
Der Schlüssel verlässt den Host nie. Der Storage-Anbieter sieht ausschließlich verschlüsselte Blobs ohne Dateinamen. Verlierst du das Repository-Passwort, sind die Daten unwiederbringlich verloren, weshalb das Passwort in einen Passwortmanager gehört und nicht nur auf den Host.
graph TD
A["systemd-Timer restic-backup.timer"] --> B{"Volume mit laufender Datenbank?"}
B -->|ja| C["Dump über docker exec erzeugen"]
B -->|nein| D["Verzeichnis direkt lesen"]
C --> E["restic backup --stdin"]
D --> F["restic backup /var/lib/docker/volumes/NAME/_data"]
E --> G["Chunking, Deduplizierung, AES-256"]
F --> G
G --> H["Verschlüsselte Blobs im S3-Repository"]
H --> I["restic forget --prune nach Aufbewahrungsregel"]
Passende Infrastruktur bei centron
Sicherungen gehören nicht auf dieselbe Maschine: cBacks legt Disk-Images automatisiert außerhalb des Hosts ab. cBacks ansehen →
Restic-Repository auf S3 anlegen
Lege zuerst Passwort und Zugangsdaten getrennt vom Skript ab. Restic liest beides aus Umgebungsvariablen, was das Passwort aus der Prozessliste heraushält:
$ sudo install -d -m 0700 /etc/restic
$ openssl rand -base64 48 | sudo tee /etc/restic/password > /dev/null
$ sudo chmod 0600 /etc/restic/passwordDie Environment-Datei /etc/restic/restic.env bündelt Repository und Credentials:
RESTIC_REPOSITORY=s3:https://<dein-s3-endpoint>/<dein-bucket>/docker-volumes
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=<dein-access-key>
AWS_SECRET_ACCESS_KEY=<dein-secret-key>
RESTIC_CACHE_DIR=/var/cache/resticAls Ziel eignet sich jeder S3-kompatible Object Storage, weil Restic ausschließlich die S3-API nutzt und die Verschlüsselung ohnehin lokal passiert. Das Backup liegt damit außerhalb des Docker-Hosts, was bei einem Dateisystemschaden auf der VM den Unterschied macht.
Rechte setzen und Repository initialisieren:
$ sudo chmod 0600 /etc/restic/restic.env
$ sudo -i
# set -a; . /etc/restic/restic.env; set +a
# restic init
created restic repository 9f2a1c4e8b at s3:https://<dein-s3-endpoint>/<dein-bucket>/docker-volumesErscheint stattdessen config file already exists, ist das Repository bereits vorhanden. Initialisiere es nicht erneut, sondern nutze es weiter.
Volumes konsistent sichern
Ein Dateikopie-Backup eines laufenden Datenbank-Volumes liefert einen inkonsistenten Zustand. Trenne deshalb zwei Fälle: reine Daten-Volumes und Volumes mit aktivem Schreibprozess.
Reine Daten-Volumes direkt sichern
Uploads, statische Assets oder Konfigurationsverzeichnisse kannst du im laufenden Betrieb lesen:
# restic backup /var/lib/docker/volumes/meinprojekt_uploads/_data \
--tag docker-volume --tag meinprojekt_uploads --host "$(hostname -f)"Nutzt ein Volume einen anderen Treiber als local, existiert kein Hostpfad. Sichere es dann aus einem Hilfscontainer heraus, der das Volume schreibgeschützt einhängt:
# docker run --rm \
-v meinprojekt_uploads:/data:ro \
-v /etc/restic:/etc/restic:ro \
--env-file /etc/restic/restic.env \
restic/restic:0.17.3 backup /data --tag meinprojekt_uploadsDatenbank-Volumes über einen Dump sichern
Für PostgreSQL und MySQL ist der logische Dump der verlässliche Weg. Restic liest ihn direkt von stdin, ohne dass eine temporäre Datei auf der Platte landet:
# docker exec -i meinprojekt-db pg_dumpall -U postgres \
| restic backup --stdin --stdin-filename pgdumpall.sql \
--tag docker-volume --tag postgresFür MariaDB oder MySQL:
# docker exec -i meinprojekt-db mysqldump --all-databases --single-transaction \
-u root -p"$MYSQL_ROOT_PASSWORD" \
| restic backup --stdin --stdin-filename mysqldump.sql --tag mysqlWenn ein Dump nicht möglich ist, etwa bei einer eingebetteten Datenbank ohne Export-Werkzeug, stoppe den Container für die Dauer des Backups:
# docker compose -f /srv/meinprojekt/compose.yaml stop app
# restic backup /var/lib/docker/volumes/meinprojekt_state/_data --tag meinprojekt_state
# docker compose -f /srv/meinprojekt/compose.yaml start appBackup automatisieren mit systemd-Timer
Lege das Backup-Skript unter /usr/local/bin/docker-volume-backup.sh an. set -euo pipefail sorgt dafür, dass ein fehlgeschlagener Dump nicht als erfolgreiches Backup durchgeht:
#!/usr/bin/env bash
set -euo pipefail
VOLUMES=("meinprojekt_uploads" "meinprojekt_config")
HOSTTAG="$(hostname -f)"
for vol in "${VOLUMES[@]}"; do
restic backup "/var/lib/docker/volumes/${vol}/_data" \
--tag docker-volume --tag "${vol}" --host "${HOSTTAG}"
done
docker exec -i meinprojekt-db pg_dumpall -U postgres \
| restic backup --stdin --stdin-filename pgdumpall.sql \
--tag docker-volume --tag postgres --host "${HOSTTAG}"
restic forget --tag docker-volume \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6 --pruneAusführbar machen und die Units anlegen:
# chmod 0700 /usr/local/bin/docker-volume-backup.sh/etc/systemd/system/restic-backup.service:
[Unit]
Description=Restic-Backup der Docker-Volumes
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
ExecStart=/usr/local/bin/docker-volume-backup.sh
Nice=10
IOSchedulingClass=idle/etc/systemd/system/restic-backup.timer:
[Unit]
Description=Taegliches Restic-Backup der Docker-Volumes
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=900
Persistent=true
[Install]
WantedBy=timers.targetTimer aktivieren und den ersten Lauf manuell auslösen:
# systemctl daemon-reload
# systemctl enable --now restic-backup.timer
# systemctl start restic-backup.service
# journalctl -u restic-backup.service -n 30 --no-pagerPersistent=true holt einen verpassten Lauf nach, wenn der Host zum geplanten Zeitpunkt nicht lief.
Aufbewahrung mit forget und prune steuern
restic forget entfernt nur die Snapshot-Referenzen, restic prune löscht die dann nicht mehr referenzierten Blobs im Repository und gibt den Speicher frei. Ohne prune wächst der Bucket dauerhaft weiter.
| Befehl | Wirkung | Laufzeit |
|---|---|---|
restic forget --keep-daily 7 |
markiert alte Snapshots als entfernt | Sekunden |
restic forget --prune |
entfernt Snapshots und gibt Speicher frei | Minuten bis Stunden |
restic snapshots |
listet vorhandene Snapshots | Sekunden |
restic stats --mode raw-data |
zeigt die tatsächliche Repository-Größe | Sekunden |
Prüfe eine Aufbewahrungsregel immer erst im Trockenlauf:
# restic forget --tag docker-volume --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-runDie Ausgabe listet unter remove genau die Snapshots, die der echte Lauf löschen würde. Stimmt die Liste nicht mit deiner Erwartung überein, korrigiere die --keep-Werte, bevor du --prune ergänzt.
Restore prüfen
Ein Backup gilt erst als funktionierend, wenn ein Restore nachweislich gelaufen ist. Liste zunächst die Snapshots und stelle einen davon in ein Testverzeichnis wieder her:
# restic snapshots --tag meinprojekt_uploads
# restic restore latest --tag meinprojekt_uploads --target /var/tmp/restore-test
# diff -r /var/tmp/restore-test/data /var/lib/docker/volumes/meinprojekt_uploads/_dataGibt diff keine Ausgabe zurück, stimmen Wiederherstellung und Live-Volume überein. Danach das Testverzeichnis wieder entfernen.
Für die Integrität des Repositories selbst prüfst du zusätzlich einen Teil der Daten stichprobenartig. Der Befehl lädt fünf Prozent der Blobs herunter und verifiziert deren Prüfsummen:
# restic check --read-data-subset=5%Einen Datenbank-Dump spielst du direkt aus dem Repository in den Container zurück:
# restic dump latest --tag postgres /pgdumpall.sql | docker exec -i meinprojekt-db psql -U postgresTroubleshooting
repository is already locked exclusively: Ein abgebrochener Lauf hat einen Lock hinterlassen. Prüfe, dass kein Restic-Prozess mehr läuft, und löse den Lock anschließend auf.
# pgrep -a restic
# restic unlockwrong password or no key found: Die Datei aus RESTIC_PASSWORD_FILE passt nicht zum Repository. Ein nachträglich neu erzeugtes Passwort öffnet ein bestehendes Repository nicht. Nutze die ursprüngliche Datei oder lege mit restic key add einen zweiten Schlüssel an, solange du noch Zugriff hast.
prune bricht mit OOM ab: Der Index eines großen Repositories liegt im RAM. Begrenze die Parallelität und führe prune seltener aus:
# GOGC=20 restic prune --max-repack-size 2GS3 antwortet mit 403 Forbidden: Meist stimmt die Systemzeit nicht, weil die S3-Signatur zeitgebunden ist. Prüfe mit timedatectl und aktiviere systemd-timesyncd oder chrony.
Fazit
Mit Restic liegen deine Docker-Volumes clientseitig verschlüsselt und dedupliziert außerhalb des Hosts. Entscheidend sind drei Punkte: konsistente Quellen über Dumps statt roher Dateikopien, eine bewusst gewählte Aufbewahrungsregel mit forget --prune und ein regelmäßig ausgeführter Testrestore. Trage das Repository-Passwort in deinen Passwortmanager ein, bevor du den Timer aktivierst.
Weitere Docker-Themen
- Ollama im Docker-Container betreiben
- Nextcloud mit Docker Compose betreiben
- Docker-Images mit mehrstufigen Builds verkleinern
Testen Sie Ihr Setup auf ccloud³
Registrieren Sie sich in der ccloud³ und erhalten Sie 200 € Startguthaben für Ihr Projekt – z. B. für eine PostgreSQL-VM mit automatischen Backups.