ZFS System Backups with zrepl on Ubuntu (ZFS root)

A clean, cron-free way to back up a ZFS-root Ubuntu server: run zrepl on a backup box and pull snapshots over TCP from the source. The source only runs a lightweight zrepl server; restores are simple via zfs clone.

Tested with:

  • Source: test.maksonlee.com — Ubuntu 24.04, ZFS root: bpool, rpool, IP 192.168.0.92
  • Backup: bkserver.maksonlee.com — Ubuntu 24.04, pool backup, IP 192.168.0.91

Security note: type: tcp is clear-text and authenticates clients by IP mapping. Use only on trusted LANs, or put the link behind WireGuard/OpenVPN/IPsec, or use zrepl’s type: tls transport for mTLS.

What you get

  • Periodic pull from backup → source over TCP
  • Replicates periodic snapshots
  • Placeholder datasets on the backup so nothing is auto-mounted
  • Pruning on both sides (useful recency grid)

  1. Install zrepl on both hosts (official repo)
(
set -ex
zrepl_apt_key_url=https://zrepl.cschwarz.com/apt/apt-key.asc
zrepl_apt_key_dst=/usr/share/keyrings/zrepl.gpg
zrepl_apt_repo_file=/etc/apt/sources.list.d/zrepl.list

# Install dependencies for subsequent commands
sudo apt update && sudo apt install -y curl gnupg lsb-release

# Deploy the zrepl apt key.
curl -fsSL "$zrepl_apt_key_url" | tee | gpg --dearmor | sudo tee "$zrepl_apt_key_dst" > /dev/null

# Add the zrepl apt repo.
ARCH="$(dpkg --print-architecture)"
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"
echo "Using Distro and Codename: $CODENAME"
echo "deb [arch=$ARCH signed-by=$zrepl_apt_key_dst] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee "$zrepl_apt_repo_file" > /dev/null

# Update apt repos & install zrepl.
sudo apt update
sudo apt install -y zrepl
zrepl version
)

  1. Open the port (source → listens, backup → connects)

On test (192.168.0.92), allow bkserver to reach TCP/8888:

# If using ufw:
sudo ufw allow from 192.168.0.91 to any port 8888 proto tcp

  1. Source config (test) — serve over TCP + take snapshots

/etc/zrepl/zrepl.yml

global:
  logging:
    - type: syslog
      format: human
      level: info

jobs:
  - type: source
    name: source_for_bkserver

    # Source takes snapshots automatically; pull will replicate them.
    snapshotting:
      type: periodic
      prefix: zrepl_
      interval: 5m

    serve:
      type: tcp
      listen: "0.0.0.0:8888"
      clients:
        "192.168.0.91": "bkserver.maksonlee.com"

    filesystems:
      "rpool/ROOT<": true
      "bpool/BOOT<": true

Apply & start:

sudo zrepl configcheck
sudo systemctl enable --now zrepl
# Confirm the listener:
ss -lntp | grep 8888 || sudo journalctl -u zrepl -n 100 --no-pager

  1. Create the backup pool & placeholders (bkserver)

These commands wipe your selected disk. Use your own by-path symlink.

# On bkserver
# Inspect disks
lsblk

# Use a stable device path for sdb (adjust the grep target to your disk)
ls -l /dev/disk/by-path/ | grep 'sdb$'

# Create pool (WILL WIPE THE DISK)
sudo zpool create -o ashift=12 -o autotrim=on \
  -O atime=off -O compression=zstd -O mountpoint=none \
  backup /dev/disk/by-path/<your-sdb-by-path-symlink>

# Optional: cold-store tuning to save ARC
sudo zfs set primarycache=metadata backup

# Keep pool root clean (not auto-mounted)
sudo zfs set canmount=off mountpoint=none backup

# Browsing root (optional)
sudo zfs create -o mountpoint=/backup backup/zrepl

# One container per source host; leaves are created by receive
sudo zfs create -o canmount=off -o mountpoint=none backup/zrepl/test

# Verify
zpool status
zpool list
zfs list -o name,mountpoint,canmount backup

  1. Pull job on backup (bkserver) — connect over TCP

/etc/zrepl/zrepl.yml on bkserver:

global:
  logging:
    - type: syslog
      format: human
      level: info

jobs:
  - type: pull
    name: pull_from_test

    connect:
      type: tcp
      address: "192.168.0.92:8888"

    # Where to receive the datasets from 'test'
    root_fs: "backup/zrepl/test"

    # Create placeholders with explicit encryption policy:
    # Option A (default in this post): unencrypted placeholders
    recv:
      placeholder:
        encryption: off

    interval: 15m

    pruning:
      keep_sender:
        - type: last_n
          count: 28
      keep_receiver:
        - type: grid
          grid: "24x1h | 7x1d | 4x1w | 12x30d"
          regex: "^zrepl_.*"

    replication:
      protection:
        initial: guarantee_resumability
        incremental: guarantee_resumability

Apply & trigger first run:

sudo zrepl configcheck
sudo systemctl enable --now zrepl
sudo zrepl signal wakeup pull_from_test

  1. Verify replication
# Watch logs
journalctl -u zrepl -n 200 --no-pager

# Confirm received snapshots
zfs list -t snapshot -r backup/zrepl/test | head -n 40

You should see fresh zrepl_... snapshots updating every 15 minutes. Pruning runs after replication.


  1. Restore (fast) via zfs clone
# 7.1 Find a snapshot on the receiver
zfs list -t snapshot -r backup/zrepl/test | head

# 7.2 Clone a snapshot under backup/zrepl/*
SNAP=backup/zrepl/test/rpool/ROOT/<your_dataset>@<your_snapshot>
sudo zfs clone "$SNAP" backup/zrepl/restore-test

# 7.3 Verify it mounted under /backup
zfs list -o name,mountpoint,mounted backup/zrepl/restore-test
ls -la /backup/restore-test | head

# 7.4 Cleanup when done
sudo zfs destroy backup/zrepl/restore-test

  1. Add more machines

For each additional source host:

  • On the source, add its backup IP to the serve.clients map with a unique identity.
  • On bkserver, create backup/zrepl/<host> (canmount=off, mountpoint=none) and add another pull job with root_fs: backup/zrepl/<host> and connect.address: "<source-ip>:8888".
  • Keep the same pruning grid or adjust per host.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top