# Proxmox ZFS Root Partition — Backup & Restore Guide
---
## Placeholders
| Placeholder | Description | Example |
|---|---|---|
| `<SOURCE_DISK>` | Drive being backed up | `nvme0n1` |
| `<TARGET_DISK>` | New/replacement drive for restore | `nvme1n1` |
| `<BACKUP_HOST>` | Remote backup server hostname | `backup.example.com` |
| `<BACKUP_USER>` | SSH user on backup server | `backup-user` |
| `<SSH_PORT>` | SSH port on backup server | `22` |
| `<BOOT_BACKUP_PATH>` | Remote path for boot partition backups | `/mnt/backups/pve/BOOT` |
| `<REMOTE_ZFS_DATASET>` | ZFS dataset on backup server containing rpool snapshot | `pool/pve/rpool-root` |
| `<SNAPSHOT_NAME>` | Name of ZFS snapshot to restore | `2024-01-01_12.00.00--3d` |
| `<SUDO_USER>` | Non-root user allowed to run ZFS commands | `zfs-backup-user` |
| `<HOST_ID>` | Output of `hostid` on the original host | `4f42e3cf` |
---
## Part 1: Backup
> Run on the **live Proxmox host**.
### 1. Install required tools
```bash
apt install -y gdisk pv zfsnap
mkdir /root/host_drive_backup
```
---
### 2. Back up partition table
Saves the full GPT layout to a binary file (a few KB). Keep this updated after any partition changes.
```bash
sgdisk --backup=/root/host_drive_backup/<SOURCE_DISK>-partition-table.bin /dev/<SOURCE_DISK>
```
| Option | Description |
|---|---|
| `--backup=<file>` | Write partition table backup to the specified file |
---
### 3. Back up BIOS Boot partition (`p1`, ~1 MB)
```bash
dd if=/dev/<SOURCE_DISK>p1 bs=1M status=progress \
| gzip > /root/host_drive_backup/<SOURCE_DISK>p1-bios-boot.img.gz
```
| Option | Description |
|---|---|
| `if=` | Input file (source partition) |
| `bs=1M` | Block size — 1 MiB; suitable for small partitions |
| `status=progress` | Show transfer progress |
| pipe to `gzip` | Compress output to save space |
---
### 4. Back up ESP/EFI partition (`p2`, ~1 GB — bootloader, kernel, initrd)
```bash
dd if=/dev/<SOURCE_DISK>p2 bs=4M status=progress \
| gzip > /root/host_drive_backup/<SOURCE_DISK>p2-esp.img.gz
```
Same options as above; larger block size (`4M`) is more efficient for bigger partitions.
---
### 5. Transfer boot backups to remote server
```bash
scp -l 409600 -P <SSH_PORT> \
/root/host_drive_backup/<SOURCE_DISK>-partition-table.bin \
/root/host_drive_backup/<SOURCE_DISK>p1-bios-boot.img.gz \
/root/host_drive_backup/<SOURCE_DISK>p2-esp.img.gz \
<BACKUP_USER>@<BACKUP_HOST>:<BOOT_BACKUP_PATH>/
```
| Option | Description |
|---|---|
| `-l 409600` | Bandwidth limit in Kbit/s (~400 Mbit/s) |
| `-P <SSH_PORT>` | SSH port |
---
### 6. Configure sudoers for non-root ZFS operations
Allows `<SUDO_USER>` to run ZFS commands without a password — required for automated snapshot/send tasks.
Create `/etc/sudoers.d/<SUDO_USER>-zfs`:
```
# /etc/sudoers.d/<SUDO_USER>-zfs
# Recursive snapshot of rpool/ROOT
<SUDO_USER> ALL=(ALL) NOPASSWD: /usr/sbin/zfs snapshot -r rpool/ROOT@*
# Full ZFS send
<SUDO_USER> ALL=(ALL) NOPASSWD: /usr/sbin/zfs send *
# List snapshots (used to find the previous snapshot in scripts)
<SUDO_USER> ALL=(ALL) NOPASSWD: /usr/sbin/zfs list *
# Read dataset properties
<SUDO_USER> ALL=(ALL) NOPASSWD: /usr/sbin/zfs get *
# Destroy old snapshots (pruning)
<SUDO_USER> ALL=(ALL) NOPASSWD: /usr/sbin/zfs destroy -r rpool/ROOT@*
# zfSnap automation
<SUDO_USER> ALL=(ALL) NOPASSWD: /usr/sbin/zfSnap *
```
> **Note:** For TrueNAS `zfs receive` setup, see: https://wiki.familybrown.org/pve-replication
> When configuring the replication task on TrueNAS, set override properties `mountpoint=none` and `canmount=off`
> to prevent auto-mounting of received datasets.
---
## Part 2: Restore
> Run from a **rescue / live Linux environment**.
### 1. Install required tools
```bash
apt update && apt install -y zfsutils-linux gdisk pv
mkdir -p /root/host_drive_backup
```
---
### 2. Identify drives
```bash
lsblk
```
Confirm `<SOURCE_DISK>` and `<TARGET_DISK>` device names before proceeding.
---
### 3. Download boot backups from remote server
```bash
scp -P <SSH_PORT> \
<BACKUP_USER>@<BACKUP_HOST>:<BOOT_BACKUP_PATH>/<SOURCE_DISK>-partition-table.bin \
<BACKUP_USER>@<BACKUP_HOST>:<BOOT_BACKUP_PATH>/<SOURCE_DISK>p1-bios-boot.img.gz \
<BACKUP_USER>@<BACKUP_HOST>:<BOOT_BACKUP_PATH>/<SOURCE_DISK>p2-esp.img.gz \
/root/host_drive_backup/
```
---
### 4. Restore partition table to new drive
```bash
sgdisk --load-backup=/root/host_drive_backup/<SOURCE_DISK>-partition-table.bin /dev/<TARGET_DISK>
```
| Option | Description |
|---|---|
| `--load-backup=<file>` | Restore GPT layout from a previously saved backup file |
---
### 5. Assign new partition GUIDs
Prevents GUID conflicts if the original drive is still present in the system.
```bash
sgdisk -G /dev/<TARGET_DISK>
```
| Option | Description |
|---|---|
| `-G` / `--randomize-guids` | Generate new random GUIDs for the disk and all partitions |
---
### 6. Verify partition layout
```bash
lsblk /dev/<TARGET_DISK>
```
Confirm that `p1`, `p2`, and `p3` are present and correctly sized.
---
### 7. Restore BIOS Boot partition
```bash
gunzip -c /root/host_drive_backup/<SOURCE_DISK>p1-bios-boot.img.gz \
| dd of=/dev/<TARGET_DISK>p1 bs=1M status=progress
```
---
### 8. Restore EFI partition
```bash
gunzip -c /root/host_drive_backup/<SOURCE_DISK>p2-esp.img.gz \
| dd of=/dev/<TARGET_DISK>p2 bs=4M status=progress
```
> **Alternative — low RAM in live environment:** Stream directly from the remote server to avoid writing to local storage:
> ```bash
> ssh <BACKUP_USER>@<BACKUP_HOST> \
> "cat <BOOT_BACKUP_PATH>/<SOURCE_DISK>p2-esp.img.gz" \
> | gunzip -c \
> | dd of=/dev/<TARGET_DISK>p2 bs=4M status=progress
> ```
---
### 9. Create rpool on the new drive
```bash
zpool create -f \
-o ashift=12 \
-o autotrim=on \
-O compression=on \
rpool /dev/<TARGET_DISK>p3
```
| Option | Description |
|---|---|
| `-f` | Force creation even if the drive contains existing data or signatures |
| `-o ashift=12` | Pool-level: 4K sector alignment — optimal for NVMe/SSD drives |
| `-o autotrim=on` | Pool-level: enable automatic TRIM for SSDs |
| `-O compression=on` | Dataset-level: enable LZ4 compression on all datasets |
---
### 10. Receive ZFS ROOT dataset from remote server
```bash
ssh -p <SSH_PORT> <BACKUP_USER>@<BACKUP_HOST> \
"sudo /sbin/zfs send -bRv <REMOTE_ZFS_DATASET>@<SNAPSHOT_NAME>" \
| zfs recv -Fv rpool/ROOT
```
| Option | Description |
|---|---|
| **`zfs send`** | |
| `-b` | Send backup stream — preserve received properties from the original pool |
| `-R` | Replicate recursively — includes all child datasets and their snapshots |
| `-v` | Verbose output |
| **`zfs recv`** | |
| `-F` | Force rollback/destroy of conflicting local data to match the incoming stream |
| `-v` | Verbose output |
---
### 11. Create data dataset
```bash
zfs create -o mountpoint=/rpool/data rpool/data
```
---
### 12. Check readonly status
```bash
zfs get readonly rpool rpool/ROOT rpool/ROOT/pve-1 rpool/data
```
If any dataset is read-only, disable it:
```bash
zfs set readonly=off rpool/ROOT/pve-1
```
---
### 13. Export pool
Required before re-importing with an alternate root for chroot.
```bash
zpool export -f rpool
```
| Option | Description |
|---|---|
| `-f` | Force export even if the pool is currently busy |
---
### 14. Import pool with alternate root
```bash
zpool import -f -R /media/RESCUE rpool
```
| Option | Description |
|---|---|
| `-f` | Force import (pool may not have been cleanly exported) |
| `-R <altroot>` | Mount all datasets under this path prefix instead of their canonical mountpoints |
---
### 15. Verify and set bootfs
```bash
zpool get bootfs
zpool set bootfs=rpool/ROOT/pve-1 rpool
```
---
### 16. Bind-mount system filesystems for chroot
```bash
mount -o rbind /proc /media/RESCUE/proc
mount -o rbind /sys /media/RESCUE/sys
mount -o rbind /dev /media/RESCUE/dev
mount -o rbind /run /media/RESCUE/run
chroot /media/RESCUE
```
| Option | Description |
|---|---|
| `-o rbind` | Recursively bind-mount, including all submounts |
---
### 17. Fix hostid and regenerate initramfs
If ZFS refuses to import the pool due to a hostid mismatch (pool was previously mounted on a different system):
```bash
hostid
# note the output value, replace <HOST_ID> below
zgenhostid -f <HOST_ID>
zpool set cachefile=/etc/zfs/zpool.cache rpool
update-initramfs -u -k all
```
| Option | Description |
|---|---|
| `zgenhostid -f` | Force overwrite of `/etc/hostid` with the given value |
| `update-initramfs -u` | Update (rather than create) existing initramfs images |
| `-k all` | Apply to all installed kernel versions |
---
### 18. Reinitialize Proxmox boot tools
```bash
proxmox-boot-tool reinit
exit
```
See also: https://pve.proxmox.com/wiki/Recover_From_Grub_Failure
---
### 19. Unmount and export (back in live/rescue Linux)
```bash
umount -R /media/RESCUE/run
umount -R /media/RESCUE/dev
umount -R /media/RESCUE/sys
umount -R /media/RESCUE/proc
```
If a process is keeping the mountpoint busy:
```bash
fuser -km /media/RESCUE
umount -lR /media/RESCUE
```
| Option | Description |
|---|---|
| `fuser -k` | Kill all processes using the mountpoint |
| `fuser -m` | Target all processes accessing the given mount |
| `umount -R` | Recursively unmount all submounts |
| `umount -l` | Lazy unmount — detach filesystem even if still busy |
---
### 20. Export pool and reboot
```bash
zpool export rpool
reboot
```