In any classic backup the awkward moment comes when the data outgrows a few gigabytes: the copying utility walks the tree file by file for hours, services keep writing to disk, and the copy ends up assembled from files of different moments in time. The Btrfs filesystem removes this contradiction at the root. Copy-on-write, or CoW, underlies all of its work, and thanks to that a subvolume snapshot is created atomically, in a fraction of a second, no matter whether the pool holds ten gigabytes or ten terabytes. The btrbk utility builds complete automation around this mechanism: it makes read-only snapshots on a schedule, sends them incrementally to a reserve disk or a remote server, rotates old copies by the configured policy and logs every transaction. The current release has number 0.32.7, the code is licensed under GPLv3. In the article, installation, a working configuration, network transfer, retention policy and recovery from a backup are taken apart, all with commands ready for copy-paste.

An atomic snapshot of the filesystem instead of file-by-file copying

A subvolume in btrfs is an independently created subtree with its own UUID, which can be snapshotted as a whole. The usual layout on a server looks like this: the root filesystem lives in the rootfs subvolume, user data in home, and the volume root with identifier 5 is mounted separately, so that all subvolumes are visible in one place. The line for /etc/fstab looks like this:

/dev/disk/by-uuid/9c14a1f2 /mnt/btr_pool btrfs subvolid=5,noatime 0 0

Mounting subvolid=5 is considered good form for backups of the root partition, and for btrbk versions below 0.32.0 it was outright mandatory. On Ubuntu the installer by default creates subvolumes with the names @ and @home, so in the configuration the paths are replaced with the corresponding ones.

A read-only snapshot is made with a single command; run it on the server with root rights:

btrfs subvolume snapshot -r /mnt/btr_pool/home /mnt/btr_pool/_btrbk_snap/home.20260720T1200

The -r flag makes the snapshot read-only and thereby protects the recovery point from accidental edits. The most interesting part: at the moment of creation nothing is copied. Only the metadata is duplicated, and the data blocks stay common between the original and the snapshot, so a fresh restore point takes up kilobytes on the disk. The disk fills up only as the original changes: the copy-on-write mechanism moves each modified block to a new place, and the old one stays in the snapshot.

Then btrfs send comes into play. This command turns the difference between the parent snapshot and the new one into a byte stream, and btrfs receive assembles an identical read-only subvolume on the receiving side. The very first transfer carries the full contents, all subsequent ones only changed extents. The arithmetic here is simple: with a pool of 500 GB and daily changes of 2 GB, the first backup will drag five hundred gigabytes, and each subsequent one a couple of gigabytes. Thirty daily copies on the reserve disk will take roughly 558 GB, and not fifteen terabytes, as would be the case with full copies. Plus each send-receive records one consistent instant in time, and not a mosaic of files read at random.

Installing btrbk and preparing the pool and directories

Btrbk is a single Perl script without compiled dependencies; for full operation it needs btrfs-progs of version 4.12 or newer and Perl, and for network transfers an ssh client. On Debian and Ubuntu the packages are installed with a standard command; run it in the terminal:

apt install btrbk mbuffer

On Fedora it is dnf install btrbk mbuffer, on Arch pacman -S btrbk mbuffer. The mbuffer utility is not required, but it is precisely it that gives btrbk a progress bar on the --progress flag and stream buffering, which per the author's measurements speeds up transfer by up to 20 percent. For buffering, a version of 20180505 or newer is needed.

Then the directories are prepared. The btrbk snapshot directory is not created automatically, and without it the first run ends in an error. Three commands solve the problem (root rights):

mkdir -p /mnt/btr_pool/_btrbk_snap
mkdir -p /mnt/btr_backup/mylaptop
btrfs subvolume create /mnt/btr_backup/mylaptop

The last command is optional: btrfs receive knows how to put subvolumes into a regular directory. But a dedicated subvolume makes life easier when the reserve disk fills up and the backups need to be moved to a new drive as one whole.

The first configuration for snapshots and transfer to a USB disk

All the logic of btrbk is described in one file, /etc/btrbk/btrbk.conf. A minimal working configuration for a laptop or a small server with a USB drive connected looks like this:

transaction_log /var/log/btrbk.log
lockfile /var/lock/btrbk.lock
timestamp_format long
snapshot_preserve_min 2d
snapshot_preserve 14d
target_preserve_min no
target_preserve 20d 10w *m
snapshot_dir _btrbk_snap
stream_buffer 256m

volume /mnt/btr_pool
target /mnt/btr_backup/mylaptop
subvolume rootfs
subvolume home

The volume section sets the base path, subvolume lists the subvolumes for backup, target points to the reserve disk. The snapshot_dir path is specified relative to the volume, and the suffix _N is appended to the snapshot name if a snapshot for that moment already exists. With timestamp_format long the names come out as home.20260720T1531, the short format gives home.20260720, and long-iso adds seconds and the time zone.

Before a real run the configuration is checked in dry-run mode, which shows the plan of actions without writing to the disk (root commands):

btrbk -n run
btrbk -n -S run

The -S flag additionally prints the schedule, that is, which copies will be preserved and which will be deleted. If the dry run is free of errors, the first real run is launched:

btrbk run

For a laptop whose drive is not always connected, the line snapshot_create ondemand is written into the configuration: then a snapshot is created only when the target is reachable, and the local disk does not get cluttered with pointlessly duplicated metadata. Another handy mode, the btrbk snapshot home command, makes an extra copy of home right in the terminal, bypassing the schedule.

Retention policy with hourly, daily, weekly and monthly copies

The retention policy in btrbk is written in a compact language of intervals. The target_preserve 20d 10w *m line is read as follows: keep daily backups for twenty days, weekly ones for ten weeks, monthly ones forever, the asterisk means "all". The copy's category is determined not by the name, but by time. The first backup of an hour counts as hourly, the first one created after the hour preserve_hour_of_day, by default midnight, counts as daily, the first daily on the day of week preserve_day_of_week, by default sunday, counts as weekly, the first weekly of a month counts as monthly, and the first monthly of a year counts as yearly. Thanks to this, one and the same policy works both with hourly runs and with daily ones.

The min options work as a safety net. By default snapshot_preserve_min and target_preserve_min are set to all, that is, btrbk deletes nothing at all until an explicit policy is defined. A sound decision: a beginner gets copies piling up, and not an empty reserve disk after the first run. The value snapshot_preserve_min 2d guarantees that any snapshot, even one made manually from the terminal, will live at least two days.

The latest pair of snapshot and backup is preserved always, regardless of the policy, and if the target turned out to be unreachable, btrbk preserves all snapshots so as not to break the incremental chain. When the disk does run out, the --wipe flag helps: it deletes everything except the latest snapshots needed for the next incremental transfer.

The actual numbers on the disk are checked with two commands (root):

btrbk stats
btrbk usage

And the btrbk extents command with the diff and exclusive keys shows how much space will be freed if a group of old copies is deleted:

btrbk extents diff /mnt/btr_backup/mylaptop/home. exclusive /mnt/btr_backup/mylaptop/home.2025

The count is done by extents, so the numbers are exact, though the command reads the whole disk and runs slowly.

Network transfer via ssh with key protection and stream compression

Btrbk transfers backups over the network with ordinary ssh, in two schemes. In the first the laptop itself pushes copies to the server, and the target is written as a URL:

target ssh://backup.example.org/mnt/btr_backup/mylaptop

In the second the server pulls snapshots from the hosts, which is more reliable: an infected laptop will not touch its own backups. The volume section is written as a URL:

volume ssh://alpha.example.org/mnt/btr_pool
target /mnt/btr_backup/alpha
subvolume home

A separate key without a password is generated for the connection, because the job runs on a schedule (on the client side):

ssh-keygen -t rsa -b 4096 -f /etc/btrbk/ssh/id_rsa -C Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. -N ""

In the configuration the path to the key is set by the ssh_identity option, and the remote user by ssh_user, root by default. On the server, login by key only is enabled for root, in /etc/ssh/sshd_config:

PermitRootLogin prohibit-password

Without additional restrictions such a key is a master key to the server. The first thing to do is to limit the addresses from which it is allowed to log in, either with a Match Address 192.168.0.42 block in sshd_config, or directly in authorized_keys:

from="192.168.0.42" ssh-rsa AAAA... Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

Even stricter, the ssh_filter_btrbk.sh script from the btrbk package is used, which allows only the btrfs commands needed for snapshots and transfers, and nothing else. A line in authorized_keys looks like this:

command="/backup/scripts/ssh_filter_btrbk.sh -l --source --delete",restrict ssh-rsa AAAA...

If logging in as root is unacceptable in principle, the option backend_remote btrfs-progs-sudo is set: btrbk will prefix remote commands with sudo -n, and in /etc/sudoers on the server rights are given to the btrfs command groups, plus the readlink and test utilities. A finer option is the btrfs-progs-btrbk backend, where individual btrfs-* binaries get linux capabilities via setcap, and rights are granted only to the btrbk group.

The stream over the network can be compressed: the stream_compress zstd option switches on zstd, and then ssh compression is automatically disabled for transfers so as not to compress the same data twice. The stream_buffer 256m option puts the mbuffer buffer between the sending and receiving sides, smoothing out dips on slow disks. If the channel needs to be shared with production traffic, rate_limit 50m limits the stream to 50 megabytes per second. Every action is logged to the file from transaction_log, and with transaction_syslog set, to syslog with the program name btrbk.

The launch scheduler with cron and systemd and run modes

The btrbk package comes with ready systemd units. btrbk.service runs btrbk run once, btrbk.timer fires daily at midnight with a spread of AccuracySec=10min and Persistent=true, that is, it fires after a missed time, for example on a laptop that was asleep all night. The timer is enabled with one command (root):

systemctl enable --now btrbk.timer

A variant with cron for the hourly snapshots, the file /etc/cron.hourly/btrbk:

#!/bin/sh
exec /usr/bin/btrbk -q run

The run command does five steps in one pass: reads the state of the filesystems, creates snapshots, transfers the missing backups incrementally from the latest common parent, deletes excess copies on the target and then excess snapshots on the source. The snapshot, resume and prune commands run the same steps separately, which is convenient for flexible schedules: snapshots hourly, transfers at night. Sections are grouped with the group option and launched selectively, btrbk run vm. Exit codes are informative: 0 means success, 2 a configuration parsing error, 3 a locked lockfile, 10 that at least one backup failed.

Beginners most often stumble over these five places:

  1. the snapshot_dir directory is not created by hand, and the first run ends with an error about a missing directory;
  2. the retention policy is left at defaults snapshot_preserve_min all and target_preserve_min all, and the disk quietly goes to zero free space;
  3. copies are renamed manually, they stop matching the btrbk naming scheme and are silently ignored;
  4. in the raw format one damaged incremental file invalidates all subsequent ones, the chain is left without a common parent;
  5. after a manual restore the subvolume is made writable with btrfs property set, the Received UUID ends up in a false state, and the next incremental transfer falls off.

For the first case, the btrbk clean command helps, deleting incomplete copies left over from a connection drop: they are recognized by the empty received_uuid field.

Data recovery, archive copies and the raw format for third-party filesystems

A backup that has never been tested in recovery is a simple diskspace waster, so the recovery procedure is worth going through on a test machine. Btrbk intentionally has no restore command, everything is done with plain btrfs (root, on the target machine):

btrbk list snapshots
btrfs send /mnt/btr_backup/mylaptop/home.20260701 | btrfs receive /mnt/btr_pool/

The received subvolume lands in place read-only. Then the broken subvolume is moved aside and a writable clone of the recovery point is created:

mv /mnt/btr_pool/home /mnt/btr_pool/home.BROKEN
btrfs subvolume snapshot /mnt/btr_pool/home.20260701 /mnt/btr_pool/home

Here is the subtlety the author warns about: the writable state is set exactly by the btrfs subvolume snapshot command and in no case by btrfs property set. The latter leaves the Received UUID in a false state, and btrbk will not be able to build the next incremental transfer. The home.20260701 pair must be kept on both disks until the first new backup, otherwise the chain will break.

If the reserve server has no btrfs, the raw format comes to the rescue: the send stream is written to a file, optionally compressed and encrypted. The configuration looks like this:

raw_target_compress xz
raw_target_encrypt gpg
gpg_keyring /etc/btrbk/gpg/pubring.kbx
gpg_recipient Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.

volume /mnt/btr_pool
subvolume home
target raw ssh://backup.example.com/backup

Each copy gives two files: the data file home.20260720.btrfs.xz.gpg and the sidecar home.20260720.btrfs.xz.gpg.info with metadata. The write block size is regulated by raw_target_block_size, by default 128K. The raw format is marked experimental in the documentation, and it has no rotation of incremental copies: a full copy has to be run manually from time to time.

A second reserve disk is easily filled with archive copies with the btrbk archive command: it recursively transfers all btrbk subvolumes from one directory to another and works on kernels starting from 4.4. Two more commands are worth memorizing. btrbk diff home.20260701 home.20260720 shows the changed files with the size in bytes and the number of modifications, the c and i flags mark compressed and inline data. btrbk origin -t /mnt/btr_backup/mylaptop/home.20260720 prints the origin tree of the copy, which is convenient for auditing a hand-assembled zoo of snapshots.

As a result the scheme is assembled in half an hour and then works by itself: instant snapshots on the source, a delta of a couple of gigabytes to the reserve disk, retention without manual cleanups. The one thing automation cannot do is to check from time to time that recovery from the copy really comes out working. For that, one line in the calendar is enough, and peace of mind becomes a matter of technique.