Skip to content
Search Fedora

    Browse this documentation

    Backup and restore on Fedora: btrfs snapshots, tar and SELinux labels

    The usual backup mistake is not “no backup” but a backup that will not restore. On Fedora there is an extra layer — SELinux labels — and a plain tar -cf throws them away.

    Everything below was measured on F45.

    LayerContentsMethod
    System statewhich packages, repository configrpm -qa list plus config files
    Files and data/etc, /home, /var/libbtrfs snapshot / tar / rsync
    Whole-machine rollbackthe entire root filesystembtrfs snapshot

    None replaces the others: a package list does not restore data, and a snapshot rollback is too blunt to fix “one wrong package”.

    btrfs snapshots: placement matters more than the command

    Section titled “btrfs snapshots: placement matters more than the command”

    F45 defaults to btrfs (see storage), where snapshots are free — they only record copy-on-write references.

    Look at the current layout:

    Terminal window
    findmnt -no FSTYPE /
    sudo btrfs subvolume list /
    btrfs
    ID 256 gen 364 top level 5 path root
    ID 257 gen 300 top level 5 path boot
    ID 258 gen 368 top level 5 path home
    ID 259 gen 368 top level 5 path var

    The trap: a snapshot nested inside its own subvolume

    Section titled “The trap: a snapshot nested inside its own subvolume”

    Doing this directly:

    Terminal window
    sudo mkdir -p /snapshots
    sudo btrfs subvolume snapshot -r / /snapshots/root-$(date +%Y%m%d)

    puts the snapshot inside the root subvolume:

    ID 260 gen 369 top level 256 path root/snapshots/root-20261027

    top level 256 is the root subvolume. The next root snapshot therefore includes the previous one, and snapshot size snowballs.

    Two snapshot placements compared: snapshots inside the root subvolume nest and grow, snapshots in a top-level snapshots subvolume stay siblings

    The correct way: a separate top-level subvolume

    Section titled “The correct way: a separate top-level subvolume”
    Terminal window
    sudo mkdir -p /mnt/top
    sudo mount -o subvolid=5 /dev/sda3 /mnt/top # mount the top level
    sudo btrfs subvolume create /mnt/top/snapshots
    sudo umount /mnt/top

    Now snapshots land in snapshots, a sibling of root:

    Terminal window
    sudo btrfs subvolume snapshot -r / /mnt/top/snapshots/root-$(date +%Y%m%d-%H%M)
    sudo btrfs subvolume list /mnt/top | grep snapshots
    ID 261 gen 371 top level 5 path snapshots
    ID 262 gen 371 top level 261 path snapshots/root-20261027-1747

    top level 261 is the snapshots subvolume — no recursion.

    How much space they use:

    Terminal window
    sudo btrfs filesystem usage /
    Overall:
    Device size: 19.90GiB
    Device allocated: 5.02GiB
    Device unallocated: 14.88GiB

    A snapshot adds nothing to allocated until the original blocks are modified and diverge. A fresh snapshot costs almost nothing.

    tar: without the right flags it drops SELinux labels

    Section titled “tar: without the right flags it drops SELinux labels”

    This is where Fedora backups break. One file, three states:

    Terminal window
    # create a file with a specific label
    sudo mkdir -p /tmp/seltest/src
    echo hi | sudo tee /tmp/seltest/src/file >/dev/null
    sudo chcon -t httpd_sys_content_t /tmp/seltest/src/file
    # archive it twice: plain, and with --selinux --xattrs
    sudo tar -cf /tmp/seltest/no-selinux.tar -C /tmp/seltest/src .
    sudo tar --selinux --xattrs -cf /tmp/seltest/with-selinux.tar -C /tmp/seltest/src .
    # extract both and compare labels
    sudo mkdir -p /tmp/seltest/a /tmp/seltest/b
    sudo tar -xf /tmp/seltest/no-selinux.tar -C /tmp/seltest/a
    sudo tar --selinux --xattrs -xf /tmp/seltest/with-selinux.tar -C /tmp/seltest/b
    sudo ls -Z /tmp/seltest/src/file /tmp/seltest/a/file /tmp/seltest/b/file
    original : unconfined_u:object_r:httpd_sys_content_t:s0
    no-flag : unconfined_u:object_r:user_tmp_t:s0
    with-flag: unconfined_u:object_r:httpd_sys_content_t:s0

    The middle line is the disaster: the label fell from httpd_sys_content_t to user_tmp_t, inherited from the destination directory. The content is intact; the service can no longer read it.

    The correct pair of commands:

    Terminal window
    sudo tar --selinux --xattrs --acls -czf backup-etc.tar.gz -C / etc
    sudo tar --selinux --xattrs --acls -xzpf backup-etc.tar.gz -C /mnt/restore

    -p preserves permission bits. --selinux and --xattrs are both required — the first carries SELinux contexts, the second carries extended attributes (including capabilities).

    Package list: the only record of system state

    Section titled “Package list: the only record of system state”
    Terminal window
    rpm -qa | wc -l
    rpm -qa --qf '%{NAME}\n' | sort > /var/backups/pkgs.txt
    631

    An F45 base system is 631 packages. To restore:

    Terminal window
    sudo dnf install $(cat /var/backups/pkgs.txt)

    You also want the repository configuration and recent transactions:

    Terminal window
    cp /etc/yum.repos.d/*.repo /var/backups/ 2>/dev/null
    ls /usr/share/dnf5/repos.d/ > /var/backups/repos-list.txt # F45 packaged repos
    sudo dnf history list | head -5
    ID Command line Date and time Action(s) Altered
    10 dnf -y install dbus-daemon 2026-10-26 18:21:45 1
    9 dnf -y install dbus-tools 2026-10-26 18:20:19 1

    One, restore SELinux labels (if they were not preserved, or the files went to a different path):

    Terminal window
    sudo restorecon -Rv /etc /var/www /home

    Two, confirm services come back:

    Terminal window
    sudo systemctl daemon-reload
    systemctl list-units --failed

    Verify the backup — the most important step

    Section titled “Verify the backup — the most important step”

    An unverified backup is not a backup. The minimum set:

    Terminal window
    # 1) can the archive be read
    tar -tzf backup-etc.tar.gz | wc -l
    # 2) can it be extracted somewhere temporary
    mkdir -p /tmp/verify && tar -xzpf backup-etc.tar.gz -C /tmp/verify
    # 3) are the key files there and correct
    diff -q /tmp/verify/etc/hostname /etc/hostname
    # 4) are the labels intact
    ls -Z /tmp/verify/etc/ssh/sshd_config

    Verifying a btrfs snapshot is even more direct — mount it and look:

    Terminal window
    sudo mount -o ro,subvol=/snapshots/root-20261028-0300 /dev/sda3 /mnt/check
    ls /mnt/check/etc | head
    sudo umount /mnt/check

    Use a systemd timer rather than cron (F45 minimal images ship no crontab); the pattern is in systemd services and timers:

    /etc/systemd/system/backup-etc.timer
    [Unit]
    Description=Nightly etc backup
    [Timer]
    OnCalendar=*-*-* 03:30:00
    Persistent=true
    [Install]
    WantedBy=timers.target

    Put the tar --selinux --xattrs --acls ... line in the matching .service. Persistent=true makes a missed run catch up after boot.