[TUTORIAL] Proxmox-Backup to restic: ResticBackup.sh und ResticRestore.sh

ivenae

Well-Known Member
Feb 11, 2022
201
99
48
42

Proxmox backups with restic: ResticBackup.sh and ResticRestore.sh​

Two bash scripts, one shared config.cfg, one restic repository on an external disk. Deduplicated, encrypted, restorable without detours - and single files reachable without unpacking hundreds of gigabytes first.

Requirement: existing snapshots​

The backup does not create ZFS snapshots of its own. For ZFS volumes - VM disks as well as LXC rootfs - it always uses the newest existing snapshot of the dataset, both for zfs send and as the read source for RAW. If there is none, the disk is skipped instead of writing a smeared image (RAW_REQUIRE_SNAPSHOT). If it is older than 24 hours you get awarning, and that state is what ends up in the backup.

So how current a backup is depends on the snapshot mechanism on the host: a cron job running cv4pve-autosnap or Proxmox's own snapshots - as long as one is taken shortly before the backup run.

Exceptions: on LVM and LVM-thin the script creates a temporary snapshot itself and removes it afterwards; on directory storage it reads live, without a consistency guarantee.

Backup medium​

  • the target is found by UUID, not by a fixed path; several UUIDs = rotating media
  • a log records which disk ran and warns when the same one was used for 30 runs straight
  • after the run: fstrim, standby (hdparm -y), unmount

Formats​

  • RAW (default): the volume block by block as an image, taken from a ZFS or a temporary LVM snapshot. Every backup stands on its own - no chain, restore in one step
  • zfs send (optional): per disk (ZFS_SEND_DISKS="100:scsi1") or for everything. Full plus differentials; a new full is forced when the last one is too old, its base is gone, or the diff grows past a set share of the full. Saves read I/O on large data disks but needs room for two complete generations - the run itself points out worthwhile candidates
  • File based: LXC rootfs via a read-only snapshot, LOCAL_DIRS, machine configs
  • a config-id tag brackets everything belonging to one run - a restore takes exactly those disks, not the ones closest in time

Space​

  • the requirement is calculated exactly, not estimated: a backup not yet in the repo needs its full source size, an existing one a percentage on top; for RAW the real allocation counts, not the logical disk size
  • if that is not enough, whole backup days are released, oldest first: a full takes its diffs, their configs and the always-full volumes with it; the newest backup of a group and anything from ZFS_SEND_DISKS stay
  • retention via KEEP_DAILY/WEEKLY/MONTHLY plus prune

Integrity and warnings​

  • restic never re-reads a chunk it has already stored, so a rotted pack would go unnoticed. Hence the rotating read-back: after RESTIC_CHECK_ROTATE_RUNS runs the whole repository has been read once, with the counter living on the medium
  • warnings that never abort: full host filesystems, long uptime, little space left on the target
  • an overview of all backups including restore size at the end of the run, also in the mail

Restore​

  • --list with filters (machine, type, name, run), optionally with size; --list-disks <VMID> for the state of one machine
  • a restore needs an explicit --target on a free ID - existing machines are never overwritten, deliberately without an override
  • --machine = latest run, --date = latest before a cut-off, --snap = exactly one; target volumes via pvesm alloc, differentials pull in their full automatically

Single files​

  • --mount <SNAP-ID>: repository via FUSE, a RAW image as a read-only loop device with its partitions mounted (LVM inside is activated), LXC/LOCAL straight as a file tree. No extra disk space needed
  • --attach-raw <SNAP-ID> --vm <VMID>: for everything the host cannot read (Windows, Storage Spaces, LUKS with the key inside the guest) - the image goes into the running VM as a SCSI disk
  • --rw puts a qcow2 overlay in front, served to the VM as /dev/nbdN by qemu-nbd; writes land there and are discarded, the backup stays untouched
  • the same VM works too: Windows keeps the copy offline because of the duplicate signature - right-click the disk, "Online", and the new signature goes into the overlay
  • Ctrl+C releases everything; after a hard kill --detach-raw catches up (VM config, loop/nbd device, FUSE mount, overlay, temp dir) - and only touches what the scripts created themselves
An hour of searching and unpacking for a single file turns into a minute.

Installation and Configuration​


Dependencies:
Code:
apt install restic jq pv

Copy all three files to /usr/local/bin/ and drop the .txt extension.
Code:
chmod +x ResticBackup.sh
chmod +x ResticRestore.sh

Find the UUID of the external disk and put it into /usr/local/bin/config.cfg.
Code:
blkid

Every commented-out parameter in config.cfg is shown with the default it uses when left
alone - the file runs as it is.

Back up all VMs, all LXC containers and the optional LOCAL_DIRS:
Code:
./ResticBackup.sh

Back up machine 115 only (--dry-run and -v work as well):
Code:
./ResticBackup.sh 115

Restore:
Code:
./ResticRestore.sh --help

# e.g.
./ResticRestore.sh --machine 115 --target 9115


Runtime:

Code:
VM:
600 GB machine storage
300 GB used
5 GB diff per day
Backup-Storage: 930 GB

               | Read I/O VM  | Write I/O Backup | max Backup count |  runtime
               |  500 MB/s    |     100 MB/s     |    on disk       |
------------------------------------------------------------------------------------
VZDump         |  600 GB      |     250 GB *     |                  |    42 min
------------------------------------------------------------------------------------
20-backup-sum  |   12 TB      |       5 TB       |       2 **       |   840 min



RAW first time |  600 GB      |     250 GB *     |                  |    42 min
RAW following  |  600 GB      |       5 GB       |                  |    20 min
------------------------------------------------------------------------------------
20-backup-sum  |   12 TB      |     345 GB       |     > 100 ***    |   422 min



zfs_s 1. full  |  300 GB      |    300 GB *     |                   |    50 min
zfs_s 1. diff  |    5 GB      |      5 GB       |                   |     1 min
zfs_s 2. diff  |   10 GB      |     10 GB       |                   |     2 min
zfs_s 3. diff  |   15 GB      |     15 GB       |                   |   2.5 min
...
zfs_s 9. diff  |   45 GB      |     45 GB       |                   |   7.5 min
zfs_s 2. full  |  300 GB      |    300 GB       |                   |    50 min
zfs_s 10. diff |    5 GB      |      5 GB       |                   |     1 min
...
------------------------------------------------------------------------------------
20-backup-sum  |  1.1 TB      |    1.1 TB       |      13 ****      |   175 min


*   300 GB used storage can compress to approx 250 GB; if you store via zfs_send, further reduction is not possible
**  I you use the prune process of VZdump, which only prune after backup your total number is 2.
    You can increment this to 3 if you use your own prune process, which prune directly before creation
*** (930 - 250)/5 = 137, but you would not conserve 137 dailys,
    so the difference between individual backups eventually become larger than 5 GB
****Initial you can add 17 backups, but worst case would be: 300+40+45+300+5+10+15+20+25+30+35+40+45 = 910 => 13 backups
 

Attachments

Last edited:
VZ-Dump with ZStd compression:
Code:
100: 2026-07-04 01:00:09 INFO: Starting Backup of VM 100 (qemu)
100: 2026-07-04 01:00:09 INFO: status = running
100: 2026-07-04 01:00:09 INFO: VM Name: Win10
100: 2026-07-04 01:00:09 INFO: include disk 'scsi1' 'local-zfs:vm-100-disk-0' 906173M
100: 2026-07-04 01:00:09 INFO: backup mode: snapshot
100: 2026-07-04 01:00:09 INFO: ionice priority: 7
100: 2026-07-04 01:00:09 INFO: snapshots found (not included into backup)
100: 2026-07-04 01:00:09 INFO: creating vzdump archive '/media/SmartStore/vzdump-qemu-100-2026_07_04-01_00_09.vma.zst'
100: 2026-07-04 01:00:09 INFO: issuing guest-agent 'fs-freeze' command
100: 2026-07-04 01:00:10 INFO: starting backup via QMP command
100: 2026-07-04 01:00:10 INFO: issuing guest-agent 'fs-thaw' command
100: 2026-07-04 01:00:12 INFO: started backup task '0cd6e0d9-c9c1-4410-97f4-db57e710aac4'
100: 2026-07-04 01:00:12 INFO: resuming VM again
100: 2026-07-04 01:00:15 INFO:  0% (913.1 MiB of 884.9 GiB) in 3s, read: 304.4 MiB/s, write: 257.9 MiB/s
100: 2026-07-04 01:00:47 INFO:  1% (9.9 GiB of 884.9 GiB) in 35s, read: 288.6 MiB/s, write: 222.9 MiB/s
100: 2026-07-04 01:00:53 INFO:  2% (17.9 GiB of 884.9 GiB) in 41s, read: 1.3 GiB/s, write: 162.1 MiB/s
100: 2026-07-04 01:01:30 INFO:  3% (26.7 GiB of 884.9 GiB) in 1m 18s, read: 245.5 MiB/s, write: 242.4 MiB/s
100: 2026-07-04 01:02:11 INFO:  4% (35.6 GiB of 884.9 GiB) in 1m 59s, read: 220.2 MiB/s, write: 219.9 MiB/s
100: 2026-07-04 01:02:50 INFO:  5% (44.5 GiB of 884.9 GiB) in 2m 38s, read: 234.1 MiB/s, write: 231.7 MiB/s
...
100: 2026-07-04 01:37:40 INFO: 96% (849.8 GiB of 884.9 GiB) in 37m 28s, read: 1.5 GiB/s, write: 157.2 MiB/s
100: 2026-07-04 01:37:45 INFO: 97% (859.8 GiB of 884.9 GiB) in 37m 33s, read: 2.0 GiB/s, write: 102.7 MiB/s
100: 2026-07-04 01:37:50 INFO: 98% (867.8 GiB of 884.9 GiB) in 37m 38s, read: 1.6 GiB/s, write: 117.8 MiB/s
100: 2026-07-04 01:37:55 INFO: 99% (879.0 GiB of 884.9 GiB) in 37m 43s, read: 2.2 GiB/s, write: 76.8 MiB/s
100: 2026-07-04 01:38:04 INFO: 100% (884.9 GiB of 884.9 GiB) in 37m 52s, read: 680.6 MiB/s, write: 125.6 MiB/s
100: 2026-07-04 01:38:04 INFO: backup is sparse: 500.24 GiB (56%) total zero data
100: 2026-07-04 01:38:04 INFO: transferred 884.93 GiB in 2272 seconds (398.8 MiB/s)
100: 2026-07-04 01:38:04 INFO: archive file size: 278.56GB
100: 2026-07-04 01:38:04 INFO: prune older backups with retention: keep-last=3
100: 2026-07-04 01:38:04 INFO: pruned 0 backup(s)
100: 2026-07-04 01:38:04 INFO: Finished Backup of VM 100 (00:37:55)


RAW -> restic (first run)
Code:
[2026-07-27T01:51:41] Base: /media/SmartStore
[2026-07-27T01:51:41] Repo: /media/SmartStore/ResticBackup
[2026-07-27T01:51:43] === Starting Proxmox backup ===
[2026-07-27T01:51:45] No LXC containers found
[2026-07-27T01:51:45] === Inventory (volumes are resolved and measured once) ===
[2026-07-27T01:51:46]  1 VM disk(s), 0 CT volume(s), 0 local dir(s) | in use: 406.2 GiB
[2026-07-27T01:51:46] === Backup mode check ===
[2026-07-27T01:51:46]  Source data in use: 406.2 GiB | target drive: 930.3 GiB
[2026-07-27T01:51:46]  Needed at AUTO_RAW_FACTOR=2.3: 934.2 GiB
[2026-07-27T01:51:46] [WARNING]  Target drive holds less than 2.3x the source data -> switching to BACKUP_MODE=raw
[2026-07-27T01:51:46] [WARNING]  Reason: a second zfs-send full would not fit; RAW images deduplicate instead of writing a second full copy
[2026-07-27T01:51:46] === Planning phase (full/diff decision) ===
[2026-07-27T01:51:46]  BACKUP_MODE=raw
[2026-07-27T01:51:46]  Result: RAW mode - every volume is stored as a full RAW image, no differentials
[2026-07-27T01:51:47]  vm-100 scsi1: RAW requires ~406.2 GiB
[2026-07-27T01:51:47] === Space required for this run ===
[2026-07-27T01:51:47]  RAW images:    406.2 GiB (always full)
[2026-07-27T01:51:47]  Total required:  406.2 GiB
[2026-07-27T01:51:47] === Space check before backup ===
[2026-07-27T01:51:47]  Required: 406.2 GiB | Free: 532.8 GiB / 930.3 GiB
[2026-07-27T01:51:47]  Enough free space -> no pre-backup pruning needed
[2026-07-27T01:51:50] Repo data size before backup: 397.4 GiB
[2026-07-27T01:51:51] --- VM 100 (Win10) ---
[2026-07-27T01:51:53]  OK: config
[2026-07-27T01:51:54]  Config-Snapshot-ID: a6691eb7
[2026-07-27T01:51:54]  RAW source: ZFS snapshot rpool/data/vm-100-disk-0@autodaily260726190001 via temporary clone rpool/data/vm-100-disk-0_rbclone
[2026-07-27T01:51:54]  RAW image scsi1: /dev/zvol/rpool/data/vm-100-disk-0_rbclone (884.9 GiB, compression=auto)
[2026-07-27T03:11:51]  OK: scsi1 raw
[2026-07-27T03:11:56] Data added this run: 252.3 GiB
[2026-07-27T03:11:56] Disk space: 280.4 GiB free of 930.3 GiB (warning threshold 10% = 93.0 GiB)
[2026-07-27T03:11:56] === Proxmox backup complete ===
[2026-07-27T03:11:56] VMs=1 CTs=0 Disks=1 (Full=0 Diff=0 Raw=1) Skipped=0 | Written=252.3 GiB | Errors=0 Warnings=2
[2026-07-27T03:11:56] === Retention: forget + prune ===
[2026-07-27T03:11:56] Policy: keep-daily=5 keep-weekly=4 keep-monthly=2
[2026-07-27T03:11:57] Nothing to forget
[2026-07-27T03:11:57] === Post-backup integrity check ===
[2026-07-27T03:11:57] Data written this run: 252.3 GiB
[2026-07-27T03:11:57] Checking: 10G (max: 10 GiB)
[2026-07-27T03:15:00] Integrity check passed
[2026-07-27T03:15:00] === Backups in the repository ===
[2026-07-27T03:15:21]  SNAP ID     Backup Time       TYPE     MACHINE    NAME                      DISK              SIZE  PARENT      CONFIG
[2026-07-27T03:15:21]  c60a9f34    2026-07-22 02:00  CONFIG   vm-100     Win10                     -            377.8 GiB  -           c60a9f34
[2026-07-27T03:15:21]  88da2d21    2026-07-22 02:00  FULL     vm-100     Win10                     scsi1        377.8 GiB  -           c60a9f34
[2026-07-27T03:15:21]  f1655cc9    2026-07-23 01:00  CONFIG   vm-100     Win10                     -            382.2 GiB  -           f1655cc9
[2026-07-27T03:15:21]  56df3135    2026-07-23 01:00  DIFF     vm-100     Win10                     scsi1        382.2 GiB  88da2d21    f1655cc9
[2026-07-27T03:15:21]  a7c8506e    2026-07-24 01:00  CONFIG   vm-100     Win10                     -            384.2 GiB  -           a7c8506e
[2026-07-27T03:15:21]  a2b2c759    2026-07-24 01:00  DIFF     vm-100     Win10                     scsi1        384.2 GiB  88da2d21    a7c8506e
[2026-07-27T03:15:21]  53fcb6f6    2026-07-25 01:00  CONFIG   vm-100     Win10                     -            386.5 GiB  -           53fcb6f6
[2026-07-27T03:15:21]  2752e956    2026-07-25 01:00  DIFF     vm-100     Win10                     scsi1        386.5 GiB  88da2d21    53fcb6f6
[2026-07-27T03:15:21]  a6691eb7    2026-07-27 01:51  CONFIG   vm-100     Win10                     -            884.9 GiB  -           a6691eb7
[2026-07-27T03:15:21]  b4acede2    2026-07-27 01:51  RAW      vm-100     Win10                     scsi1        884.9 GiB  -           a6691eb7

RAW -> restic (second run)
Code:
[2026-07-28T01:00:05] Base: /media/SmartStore
[2026-07-28T01:00:05] Repo: /media/SmartStore/ResticBackup
[2026-07-28T01:00:07] === Starting Proxmox backup ===
[2026-07-28T01:00:08] No LXC containers found
[2026-07-28T01:00:08] === Inventory (volumes are resolved and measured once) ===
[2026-07-28T01:00:09]  1 VM disk(s), 0 CT volume(s), 0 local dir(s) | in use: 408.0 GiB
[2026-07-28T01:00:09] === Backup mode check ===
[2026-07-28T01:00:09]  Source data in use: 408.0 GiB | target drive: 930.3 GiB
[2026-07-28T01:00:09]  Needed at AUTO_RAW_FACTOR=2.3: 938.3 GiB
[2026-07-28T01:00:09] [WARNING]  Target drive holds less than 2.3x the source data -> switching to BACKUP_MODE=raw
[2026-07-28T01:00:09] [WARNING]  Reason: a second zfs-send full would not fit; RAW images deduplicate instead of writing a second full copy
[2026-07-28T01:00:09] === Planning phase (full/diff decision) ===
[2026-07-28T01:00:09]  BACKUP_MODE=raw
[2026-07-28T01:00:09]  Result: RAW mode - every volume is stored as a full RAW image, no differentials
[2026-07-28T01:00:15]  vm-100 scsi1: RAW requires ~25.2 GiB
[2026-07-28T01:00:15] === Space required for this run ===
[2026-07-28T01:00:15]  RAW images:    25.2 GiB (always full)
[2026-07-28T01:00:15]  Total required:  25.2 GiB
[2026-07-28T01:00:15] === Space check before backup ===
[2026-07-28T01:00:15]  Required: 25.2 GiB | Free: 280.4 GiB / 930.3 GiB
[2026-07-28T01:00:15]  Enough free space -> no pre-backup pruning needed
[2026-07-28T01:00:19] Repo data size before backup: 649.7 GiB
[2026-07-28T01:00:20] --- VM 100 (Win10) ---
[2026-07-28T01:00:23]  OK: config
[2026-07-28T01:00:23]  Config-Snapshot-ID: 60472f6c
[2026-07-28T01:00:24]  RAW source: ZFS snapshot rpool/data/vm-100-disk-0@autodaily260727190001 via temporary clone rpool/data/vm-100-disk-0_rbclone
[2026-07-28T01:00:24]  RAW image scsi1: /dev/zvol/rpool/data/vm-100-disk-0_rbclone (884.9 GiB, compression=auto)
[2026-07-28T01:26:21]  OK: scsi1 raw
[2026-07-28T01:26:28] Data added this run: 4.1 GiB
[2026-07-28T01:26:28] Disk space: 276.3 GiB free of 930.3 GiB (warning threshold 10% = 93.0 GiB)
[2026-07-28T01:26:28] === Proxmox backup complete ===
[2026-07-28T01:26:28] VMs=1 CTs=0 Disks=1 (Full=0 Diff=0 Raw=1) Skipped=0 | Written=4.1 GiB | Errors=0 Warnings=2
[2026-07-28T01:26:28] === Retention: forget + prune ===
[2026-07-28T01:26:28] Policy: keep-daily=5 keep-weekly=4 keep-monthly=2
[2026-07-28T01:26:29] Nothing to forget
[2026-07-28T01:26:29] === Post-backup integrity check ===
[2026-07-28T01:26:29] Data written this run: 4.1 GiB
[2026-07-28T01:26:29] Checking: 5G (max: 10 GiB)
[2026-07-28T01:28:08] Integrity check passed
[2026-07-28T01:28:08] === Backups in the repository ===
[2026-07-28T01:28:34] SNAP ID   Backup Time    TYPE   MACHINE  NAME           DISK       SIZE PARENT   CONFIG
...
[2026-07-28T01:28:34] b4acede2  2026-07-27 01:51 RAW   vm-100   Win10           scsi1    884.9 GiB -      a6691eb7
[2026-07-28T01:28:34] 60472f6c  2026-07-28 01:00 CONFIG  vm-100   Win10           -      884.9 GiB -      60472f6c
[2026-07-28T01:28:34] 066becbe  2026-07-28 01:00 RAW   vm-100   Win10           scsi1    884.9 GiB -      60472f6c


Recovery of a zfs_send backup (i recommend using default RAW mode)
Code:
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
 
Last edited: