Proxmox VM + LXC -> restic

Proxmox-Backup mit ZFS und Restic

Wer virtuelle Maschinen und LXC-Container unter Proxmox betreibt, steht früher oder später vor der Frage nach einem verlässlichen, platzsparenden Backup. Genau dafür ist dieses Skript gedacht: Es sichert VMs, Container und ausgewählte Verzeichnisse automatisiert auf eine externe Festplatte.

Werden Maschinen im Format RAW gesichert, spielt es die Stärke durch die Deduplizierung der RAW Images mit restic aus: Statt täglich ein Full-Backup mit VZDump anzulegen, kann das RAW Image mit restic außerordentlich gut dedupliziert werden, sodass die täglich geschriebene Änderung in vielen Use-Cases nur wenige Gigabyte pro Tag bedeuten. Eine 350 Gigabyte VM kann auf einer 500 GB Platte in 15 alten Versionsständen vorgehalten werden, ohne das jemals der Datenträger volläuft.

Werden Maschinen im Format zfssend gesichert, ist die Deduplizierung nicht möglich, aber die Nutzung von Snapshot Diffs führt dennoch zu erheblich kleineren Sicherungen als stetige VZDump Sicherungen, und spart nicht nur Schreiblast auf der Zielplatte, sondern auch erhebliche Lese-Vorgänge auf dem Host. Damit der Zieldatenträger nicht volläuft, muss die Backup-Festplatte jedoch mindestens 3x so groß sein wie die zu sichernde Maschine.

 

Voraussetzung: bestehende Snapshots

Das Backupskript legt selbst keine ZFS-Snapshots an. Für ZFS-Volumes - VM-Disks wieLXC-rootfs - nimmt es immer den neuesten vorhandenen Snapshot des Datasets, sowohl für zfs send als auch als Lesequelle für RAW. Fehlt einer, wird die Platte übersprungen,statt ein verschmiertes Image zu schreiben (RAW_REQUIRE_SNAPSHOT). Ist er älter als 24Stunden, gibt es eine Warnung - gesichert wird genau dieser Stand.

Die Aktualität der Sicherung hängt damit am Snapshot-Mechanismus des Hosts: ein Cronjob mit cv4pve-autosnap oder die Snapshots von Proxmox selbst -Hauptsache, kurz vor dem Backuplauf entsteht einer.

Ausnahmen: auf LVM und LVM-thin legt das Skript selbst einen temporären Snapshot an und räumt ihn wieder ab, auf Directory-Storage wird live gelesen, ohne Konsistenzgarantie.

Sicherungsmedium​

  • Ziel wird über die UUID gesucht, nicht über einen festen Pfad; mehrere UUIDs = rotierende Medien
  • Log hält fest, welche Platte lief, und warnt, wenn 30 Läufe lang immer dieselbe steckte
  • nach dem Lauf fstrim, Standby (hdparm -y), unmount

 

Formate​

  • RAW (Standard): Volume blockweise als Image, aus ZFS- oder temporärem LVM-Snapshot. Jedes Backup steht für sich – keine Kette, Restore in einem Schritt
  • zfs send (optional): pro Platte (ZFS_SEND_DISKS="100:scsi1") oder für alles. Full + Diffs; neues Full bei zu altem Full, fehlender Basis oder zu großem Diff. Spart Lese-I/O auf großen Datenplatten, braucht aber Platz für zwei Generationen – lohnende Kandidaten meldet der Lauf selbst
  • Dateibasiert: LXC-rootfs über ro-Snapshot, LOCAL_DIRS, Maschinenconfigs
  • Ein config-id-Tag klammert alles zusammen, was zu einem Lauf gehört – der Restore nimmt genau diese Platten

Platz​

  • Bedarf wird exakt gerechnet, nicht geschätzt: neues Backup = volle Quellgröße, vorhandenes = Aufschlag in Prozent; bei RAW zählt die echte Belegung
  • reicht es nicht, werden ganze Backup-Tage freigegeben, alt nach neu: ein Full nimmt Diffs, Configs und immer-full-Volumes mit; neuestes Backup einer Gruppe und ZFS_SEND_DISKS bleiben
  • Aufbewahrung über KEEP_DAILY/WEEKLY/MONTHLY plus prune

 

Integrität und Warnungen​

  • restic liest gespeicherte Chunks nie wieder – ein verrottetes Pack fiele nicht auf. Deshalb rotierendes Zurücklesen: nach RESTIC_CHECK_ROTATE_RUNS Läufen ist das Repo einmal komplett geprüft, Zähler pro Medium
  • Warnungen ohne Abbruch: volle Host-Dateisysteme, lange Uptime, knapper Platz am Ziel
  • Übersicht aller Backups samt Wiederherstellungsgröße am Ende, auch in der Mail

 

Restore​

  • --list mit Filtern (Maschine, Typ, Name, Lauf), optional mit Größe; --list-disks <VMID> für den Stand einer Maschine
  • Restore nur mit explizitem --target auf eine freie ID – bestehende Maschinen werden nie überschrieben, bewusst ohne Override
  • --machine = letzter Lauf, --date = letzter vor Stichtag, --snap = genau einer; Zielvolumes per pvesm alloc, Diffs ziehen ihr Full mit

 

Einzelne Dateien​

  • --mount <SNAP-ID>: Repo per FUSE, RAW-Image als ro-Loop-Device mit gemounteten Partitionen (LVM darin wird aktiviert), LXC/LOCAL direkt als Dateibaum. Kein zusätzlicher Plattenplatz
  • --attach-raw <SNAP-ID> --vm <VMID>: für alles, was der Host nicht lesen kann (Windows, Storage Spaces, LUKS mit Schlüssel im Gast) – Image als SCSI-Platte in die laufende VM
  • --rw schiebt einen qcow2-Overlay davor, den qemu-nbd als /dev/nbdN bereitstellt; Schreibvorgänge landen dort und werden verworfen, das Backup bleibt unberührt
  • dieselbe VM geht mit: Windows hält die Kopie wegen doppelter Signatur offline, Rechtsklick auf den Datenträger → "Online" schreibt die neue Signatur in den Overlay
  • Strg+C räumt vollständig auf; nach einem harten Abbruch holt --detach-raw das nach (VM-Config, Loop/nbd, FUSE-Mount, Overlay, Temp) – erkannt wird nur, was die Skripte selbst angelegt haben

 

Installation und Konfiguration

Abhängigkeiten:

apt install restic jq pv

 

Alle drei Dateien nach /usr/local/bin/ kopieren und .txt Endung entfernen.

chmod +x ResticBackup.sh
chmod +x ResticRestore.sh

 

UUID der ext. Festplatte herausfinden und in die /usr/local/bin/config.cfg  eintragen.

blkid

 

Alle auskommentierten Parameter in der Restic Restore entsprechen dem optionalen Default.
Sicherung aller VMs, LXC Container und den optionalen zu sichernden LOCAL_DIRS durchführen:

./ResticBackup.sh

Sicherung von Maschine 115 durchführen:

./ResticBackup.sh 115

 

Restore

./ResticRestore.sh --help
# z.B.
./ResticRestore.sh --machine 115 --target 9115

 

Download

ResticBackup.sh

ResticRestore.sh

config.cfg

 

In Action

Full Backup (mit einer etwas älteren Version, daher etwas andere Bedingungen für notwendigen Puffer)

[2026-07-09T22:20:17+0200] [INFO] Password file not found, auto-creating: /media/SmartStore/ResticBackup.pw
[2026-07-09T22:20:17+0200] [INFO] Password generated (32 chars, saved with chmod 600)
[2026-07-09T22:20:17+0200] [WARNING] IMPORTANT: Save this password elsewhere, it is needed for restore!
[2026-07-09T22:20:17+0200] [WARNING]   cat '/media/SmartStore/ResticBackup.pw'
[2026-07-09T22:20:17+0200] [INFO] Repo: /media/SmartStore/ResticBackup
[2026-07-09T22:20:17+0200] [INFO] Restic repo not found, auto-initializing: /media/SmartStore/ResticBackup
[2026-07-09T22:20:19+0200] [INFO] Restic repo initialized and ready
[2026-07-09T22:20:20+0200] [INFO] Repo data size before backup: 0 B
[2026-07-09T22:20:20+0200] [INFO] === Starting Proxmox backup ===
[2026-07-09T22:20:21+0200] [INFO] --- VM 100 (vDC01) ---
[2026-07-09T22:20:24+0200] [INFO]   Disk count changed: repo has 0, VM has 4 -> forcing full backup
[2026-07-09T22:20:24+0200] [INFO]   Disk count changed -> Full backup
[2026-07-09T22:20:24+0200] [INFO]   Full backup: efidisk0
[2026-07-09T22:20:25+0200] [INFO]   OK: efidisk0 full (rpool/data/vm-100-disk-0)
[2026-07-09T22:20:26+0200] [INFO]   Disk count changed -> Full backup
[2026-07-09T22:20:26+0200] [INFO]   Full backup: scsi0
[2026-07-09T23:28:06+0200] [INFO]   OK: scsi0 full (rpool/data/vm-100-disk-4)
[2026-07-09T23:28:07+0200] [INFO]   Disk count changed -> Full backup
[2026-07-09T23:28:07+0200] [INFO]   Full backup: scsi1
[2026-07-09T23:40:45+0200] [INFO]   OK: scsi1 full (rpool/data/vm-100-disk-1)
[2026-07-09T23:40:45+0200] [INFO]   Disk count changed -> Full backup
[2026-07-09T23:40:45+0200] [INFO]   Full backup: tpmstate0
[2026-07-09T23:40:47+0200] [INFO]   OK: tpmstate0 full (rpool/data/vm-100-disk-2)
[2026-07-09T23:40:48+0200] [INFO]   OK: config
[2026-07-09T23:40:48+0200] [INFO] No LXC containers found
[2026-07-09T23:40:51+0200] [INFO] Data added this run: 354.4 GiB
[2026-07-09T23:40:51+0200] [INFO] Disk space: 538.6 GiB free of 893.1 GiB (538.6 GiB), need 443.7 GiB (10% + written)
[2026-07-09T23:40:51+0200] [INFO] === Proxmox backup complete ===
[2026-07-09T23:40:51+0200] [INFO] VMs=1 CTs=0 Disks=4 (Full=4 Incr=0) Skipped=0 | Written=354.4 GiB | Errors=0 Warnings=2
[2026-07-09T23:40:51+0200] [INFO] === Retention: forget + prune ===
[2026-07-09T23:40:51+0200] [INFO] Policy: keep-daily=10 keep-weekly=6 keep-monthly=6
[2026-07-09T23:40:51+0200] [INFO] Protecting full backups referenced by kept incrementals
[2026-07-09T23:40:52+0200] [INFO] Nothing to forget
[2026-07-09T23:40:52+0200] [INFO] === Post-backup integrity check ===
[2026-07-09T23:40:52+0200] [INFO] Data written this run: 354.4 GiB
[2026-07-09T23:40:52+0200] [INFO] Checking: 10G (max: 10 GiB)
[2026-07-09T23:41:19+0200] [INFO] Integrity check passed

Diff Backup. Statt 1,5 Stunden benötigt es nur 1 min.

Sichere UUID f0876d4c-8a68-4afe-8c3d-a5c6ae8e59dd
[2026-07-13T12:30:01+0200] [INFO] Base: /media/SmartStore
[2026-07-13T12:30:01+0200] [INFO] Repo: /media/SmartStore/ResticBackup
[2026-07-13T12:30:05+0200] [INFO] Repo data size before backup: 354.4 GiB
[2026-07-13T12:30:05+0200] [INFO] === Starting Proxmox backup ===
[2026-07-13T12:30:05+0200] [INFO] === Planungsphase (Full/Diff-Bestimmung) ===
[2026-07-13T12:30:13+0200] [INFO]   Alle Disks differentiell sicherbar -> kein erzwungenes Full
[2026-07-13T12:30:13+0200] [INFO] === Platzpruefung vor Backup ===
[2026-07-13T12:30:13+0200] [INFO]   Diff-/LXC-/LOCAL-Lauf -> Diff-Run/No VM
[2026-07-13T12:30:13+0200] [INFO]   [Diff-Run/No VM] Frei: 538.6 GiB / 893.1 GiB  |  Puffer (5%): 44.7 GiB
[2026-07-13T12:30:13+0200] [INFO]   [Diff-Run/No VM] Genug frei (>= Puffer) -> kein Pruning noetig
[2026-07-13T12:30:14+0200] [INFO] --- VM 100 (vDC01) ---
[2026-07-13T12:30:15+0200] [INFO]   OK: config
[2026-07-13T12:30:16+0200] [INFO]   Config-Snapshot-ID: 48cf4b7e
[2026-07-13T12:30:18+0200] [INFO]   Differential: autodaily260709190002 -> autohourly260713100002
[2026-07-13T12:30:19+0200] [INFO]   OK: efidisk0 diff (rpool/data/vm-100-disk-0)
[2026-07-13T12:30:22+0200] [INFO]   Age: differential is 3 day(s) after full (a9c732da)
[2026-07-13T12:30:23+0200] [INFO]   Differential: autodaily260709190002 -> autohourly260713100002
[2026-07-13T12:30:26+0200] [INFO]   OK: scsi0 diff (rpool/data/vm-100-disk-4)
[2026-07-13T12:30:29+0200] [INFO]   Age: differential is 3 day(s) after full (bec955ba)
[2026-07-13T12:30:30+0200] [INFO]   Differential: autodaily260709190002 -> autohourly260713100002
[2026-07-13T12:30:47+0200] [INFO]   OK: scsi1 diff (rpool/data/vm-100-disk-1)
[2026-07-13T12:30:50+0200] [INFO]   Age: differential is 3 day(s) after full (03834f68)
[2026-07-13T12:30:52+0200] [INFO]   Differential: autodaily260709190002 -> autohourly260713100002
[2026-07-13T12:30:53+0200] [INFO]   OK: tpmstate0 diff (rpool/data/vm-100-disk-2)
[2026-07-13T12:30:56+0200] [INFO]   Age: differential is 3 day(s) after full (6c5b8dfd)
[2026-07-13T12:30:56+0200] [INFO] No LXC containers found
[2026-07-13T12:30:59+0200] [INFO] Data added this run: 1.8 GiB
[2026-07-13T12:30:59+0200] [INFO] Disk space: 536.8 GiB frei von 893.1 GiB (Warnschwelle 10% = 89.3 GiB)
[2026-07-13T12:30:59+0200] [INFO] === Proxmox backup complete ===
[2026-07-13T12:30:59+0200] [INFO] VMs=1 CTs=0 Disks=4 (Full=0 Diff=4) Skipped=0 | Written=1.8 GiB | Errors=0 Warnings=0
[2026-07-13T12:30:59+0200] [INFO] === Retention: forget + prune ===
[2026-07-13T12:30:59+0200] [INFO] Policy: keep-daily=10 keep-weekly=3 keep-monthly=2
[2026-07-13T12:30:59+0200] [INFO] Protecting full backups referenced by kept differentials
[2026-07-13T12:31:00+0200] [INFO] Nothing to forget
[2026-07-13T12:31:00+0200] [INFO] === Post-backup integrity check ===
[2026-07-13T12:31:00+0200] [INFO] Data written this run: 1.8 GiB
[2026-07-13T12:31:00+0200] [INFO] Checking: 2G (max: 10 GiB)
[2026-07-13T12:31:19+0200] [INFO] Integrity check passed
fstrim /media/SmartStore
fstrim: /media/SmartStore: the discard operation is not supported
fstrim not supported
Unmount /media/SmartStore
Setze standby: /dev/sda

/dev/sda:
 issuing standby command
 
Auch hier: Das Full Backup wurde mit einer alten Version erstellt, daher fehlen noch ein paar Tags und die Größe kann nicht ermittelt werden.
Die Größe der Config-Datei entspricht der Größe aller Datenträger des Diffs + aller Datenträger des zugehörigen Full Backups.
 
root:/usr/local/bin# ./ResticRestore.sh --list --size
SNAP ID     Backup Time       TYPE     MACHINE    NAME                      DISK              SIZE  PARENT      CONFIG
ca828acf    2026-07-11 02:32  CONFIG   vm-100     Server                    -            504.33GiB  -           ca828acf
a928b4dc    2026-07-11 02:32  FULL     vm-100     Server                    scsi1        504.33GiB  -           ca828acf
facd9984    2026-07-11 04:12  CONFIG   vm-101     Client                    -             81.48GiB  -           facd9984
0b9e3348    2026-07-11 04:12  FULL     vm-101     Client                    scsi0         81.48GiB  -           facd9984
493e4519    2026-07-11 23:42  CONFIG   vm-100     Server                    -            506.57GiB  -           493e4519
7667631f    2026-07-11 23:42  DIFF     vm-100     Server                    scsi1        506.57GiB  a928b4dc    493e4519
3cadf33d    2026-07-11 23:42  CONFIG   vm-101     Client                    -             82.94GiB  -           3cadf33d
592c8e47    2026-07-11 23:42  DIFF     vm-101     Client                    scsi0         82.94GiB  0b9e3348    3cadf33d
6b6dacc0    2026-07-14 01:00  CONFIG   vm-100     Server                    -            508.04GiB  -           6b6dacc0
5cee0ad5    2026-07-14 01:00  DIFF     vm-100     Server                    scsi1        508.04GiB  a928b4dc    6b6dacc0
cda6558a    2026-07-14 01:01  CONFIG   vm-101     Client                    -             83.08GiB  -           cda6558a
6557ead4    2026-07-14 01:01  DIFF     vm-101     Client                    scsi0         83.08GiB  0b9e3348    cda6558a



root:/usr/local/bin# ./ResticRestore.sh --target 667 --machine 101
Restic Restore V 4.3.6
[01:26:58] [INFO] Machine: vm-101 -> Target: 667
[01:26:58] [INFO] Config snapshot: cda6558a
[01:26:58] [INFO] Restoring backup run config-id=cda6558a
[01:26:58] [INFO] === Restoring VM 101 -> 667 ===
repository 622c9c8a opened (version 2, compression level auto)
[0:01] 100.00%  28 / 28 index files loaded
restoring snapshot cda6558a of [/vm-101.conf] at 2026-07-14 01:01:23.907287838 +0200 CEST by root to /tmp/restic-restore-cfg-3733521
Summary: Restored 1 files/dirs (581 B) in 0:00
[01:27:01] [INFO]   Config: /etc/pve/qemu-server/667.conf
[01:27:01] [INFO] Restoring disks ...
[01:27:12] [INFO]   scsi0: Full (0b9e3348, 81.48GiB) + Differential (6557ead4, 1.60GiB)
scsi0-full: 81.5GiB 0:19:17 [72.1MiB/s] [================================================================================================================================================================>] 100%
scsi0-diff: 1.60GiB 0:00:24 [67.8MiB/s] [================================================================================================================================================================>] 100%
[01:46:57] [INFO]   OK: scsi0 (full + differential)
[01:46:57] [INFO] === VM 667 restore complete ===
[01:46:57] [INFO] Start with: qm start 667