Snapper restore points on Fedora (Btrfs) with auto snapshots before every dnf transaction

Published 11 July 2026

date
updated
env
linux Fedora 44 — Btrfs-on-LUKS, anaconda multi-subvolume layout (root, home, opt, cache, log, spool, tmp, containers, gdm, libvirt), dnf5 5.4.2.1, snapper 0.13.0 macos N/A

snapper manages Btrfs snapshots — lightweight, copy-on-write “restore points” you can browse, diff, restore individual files from, or roll back to entirely. On a Btrfs root it costs almost nothing to keep dozens of them.

This how-to sets up three snapshot sources:

  1. Timeline — an automatic snapshot every hour (with auto-cleanup so they don’t pile up).
  2. Before/after every dnf transaction — an automatic pre/post pair around each install / upgrade / remove.
  3. Manual — on demand, before doing anything risky.

The catch on Fedora 44: the classic “snapshot before updates” plugin (python3-dnf-plugin-snapper) only hooks into the old dnf4, not dnf5 (the default). There is no dnf5 snapper plugin. We solve this with the generic libdnf5-plugin-actions plugin, which can run any command on transaction hooks.

Two things about Fedora specifically are worth knowing before you run snapper create-config, because both are awkward to retrofit:

  • A root config snapshots the root subvolume and nothing else — and Fedora’s installer splits a lot of the system into separate subvolumes. See What a root config actually snapshots.
  • snapper rollback does not work on a stock Fedora install, and fails silently rather than loudly. See Rolling back the whole system. The layout choice in Step 1b is what makes the procedure that does work survivable.

Prerequisites

  • Fedora with a Btrfs root filesystem. Check with:
    Terminal window
    findmnt -no FSTYPE /
    It must print btrfs. (LUKS underneath is fine — this guide was written on a Btrfs-on-LUKS install.) If it prints ext4 or xfs, snapper’s Btrfs backend does not apply and this guide won’t work as written.
  • A subvolume layout where / is its own subvolume — the Fedora default, where / is the subvolume named root. Inspect the full picture with:
    Terminal window
    sudo btrfs subvolume list /
    findmnt -t btrfs -o TARGET,SOURCE,OPTIONS
    Read the next section before going further: on Fedora that list is longer than most guides assume, and it determines what your restore points are worth.
  • sudo / root privileges.
  • No conflicting snapshot tool actively managing the same subvolumes (e.g. Timeshift in Btrfs mode). Running both leads to confusing, overlapping snapshots.

What a root config actually snapshots

Btrfs snapshots are not recursive. A snapshot of a subvolume stops at the boundary of any subvolume nested inside it: the nested one is not captured, and in the snapshot its mount point is just an empty directory. So snapper -c root create captures the root subvolume — not “the whole system”.

That matters more on Fedora than on most distributions, because anaconda splits the system into a dozen subvolumes. A stock Fedora 44 install looks like this (sudo btrfs subvolume list /, cross-checked against /etc/fstab):

SubvolumeMounted atIn a root snapshot?
root/yes — this is what you are snapshotting
home/homeno
opt/optno
cache/var/cacheno
log/var/logno
spool/var/spoolno
tmp/var/tmpno
containers/var/lib/containersno
gdm/var/lib/gdmno
libvirt/var/lib/libvirtno
var/lib/machines/var/lib/machinesno — nested inside root

The exclusions are mostly a feature. /etc, /usr, /root, /var/lib/rpm and /var/lib/dnf — everything a dnf transaction actually changes — stay in root, while container images, VM disks, logs and caches do not. Your restore points stay small and comparing a pre/post pair shows package changes instead of log churn.

Three consequences to keep in mind:

  • /opt is outside. Software installed there is not restored by a rollback. A package touching both /usr and /opt will be half-reverted: /usr goes back, /opt stays current. Rare on Fedora, but it is the one exclusion that can produce an inconsistent system rather than merely an unprotected one.
  • /home is outside — deliberately. Restore points are for the system; user data belongs in a real backup. See the notes at the end for adding a home config if you want one anyway.
  • /var/lib/flatpak is inside. It is not a separate subvolume on a stock Fedora install, and it is easily several gigabytes. Check yours:
    Terminal window
    findmnt /var/lib/flatpak # no output = not a separate subvolume, so it IS snapshotted
    sudo du -sh /var/lib/flatpak
    Every flatpak update replaces runtimes inside root, and each retained snapshot pins the previous versions. Snapshots are copy-on-write, so this costs nothing at rest — but obsolete runtimes stop being freed when you update, and the space only comes back once the last snapshot referencing them is cleaned up. If you keep many snapshots and use Flatpak heavily, either keep retention short or give /var/lib/flatpak its own subvolume the way anaconda did for containers and libvirt.

Nothing here requires action before continuing. It tells you what a restore point covers — and what it does not.

Step 1 — Install snapper and create the root config

1a. Install and create the config

Terminal window
sudo dnf install -y snapper

Create a config named root for the / subvolume. This writes /etc/snapper/configs/root and creates a /.snapshots subvolume to hold the snapshots:

Terminal window
sudo snapper -c root create-config /

1b. Move /.snapshots to a top-level subvolume

create-config puts /.snapshots inside root, as a nested subvolume. That is snapper’s default, and it is a trap on Fedora — for one reason: the rollback procedure that works on this layout (see Rolling back the whole system) replaces the root subvolume wholesale. With the default layout your entire snapshot history lives inside the subvolume you are replacing, so recovering means carefully preserving the history through the swap. Moving .snapshots up one level makes it independent of root, and rollback becomes a two-command operation.

Do this now, while there is nothing to lose — create-config has just run and no snapshots exist yet.

Delete the nested subvolume snapper just made:

Terminal window
sudo btrfs subvolume delete /.snapshots

Find the filesystem’s device node and UUID:

Terminal window
findmnt -no SOURCE / | sed 's/\[.*//' # → /dev/mapper/luks-<uuid>
findmnt -no UUID / # → the Btrfs filesystem UUID for fstab

Mount the top level of the filesystem (subvolid=5 is the root of the Btrfs tree, above every named subvolume) and create .snapshots there:

Terminal window
sudo mkdir -p /mnt/btrfs-top
sudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-top
ls /mnt/btrfs-top # root, home, opt, cache, log, ...
sudo btrfs subvolume create /mnt/btrfs-top/.snapshots
sudo umount /mnt/btrfs-top
sudo rmdir /mnt/btrfs-top

Back up /etc/fstab before touching it. On a Btrfs-on-LUKS root, a malformed fstab line does not give you a second chance at a login prompt — it drops the next boot into an emergency shell:

Terminal window
sudo cp -a /etc/fstab /etc/fstab.bak-snapper

Then add the entry, mirroring the options anaconda used on the other Btrfs lines of your fstab (Fedora’s generated entries carry x-systemd.device-timeout=0 and no compression option — copy whatever your / line has rather than the example verbatim):

UUID=<btrfs-uuid> /.snapshots btrfs subvol=.snapshots,x-systemd.device-timeout=0 0 0

Then create the mount point, mount it, and fix the SELinux label:

Terminal window
sudo mkdir -p /.snapshots
sudo chmod 750 /.snapshots
sudo systemctl daemon-reload
sudo mount -a
sudo restorecon -Rv /.snapshots

The restorecon is not defensive boilerplate — the directory really is unlabelled at that point, and restorecon prints exactly what it fixed:

Relabeled /.snapshots from system_u:object_r:unlabeled_t:s0 to system_u:object_r:snapperd_data_t:s0

The reason is worth understanding, because it tells you when you need this step and when you don’t. A subvolume’s root inode is labelled where it is created, and this one was created at /mnt/btrfs-top/.snapshots — a temporary path no SELinux rule matches, hence unlabeled_t. Mounting it at /.snapshots afterwards exposes that stored label; the mount does not relabel anything. By contrast, a .snapshots that snapper creates in place (as it does for a /home config, Step 5) comes out correctly labelled snapperd_data_t and needs no restorecon at all.

Now validate the whole fstab before you ever reboot. This is the step that turns a typo into a caught error instead of an emergency shell:

Terminal window
sudo findmnt --verify --verbose

It walks every entry (target exists, UUID resolves, filesystem type matches) and must end with Success, no errors or warnings detected.

Finally, verify that /.snapshots is a real mount of a top-level subvolume:

Terminal window
findmnt /.snapshots
stat -c '%i' /.snapshots
sudo btrfs subvolume list / | grep -E 'snapshots|top level 5'
systemctl list-units --type=mount | grep -i snapshots
  • findmnt must print a line with subvol=/.snapshots — unlike the default layout, where it prints nothing.
  • stat must print 256: on Btrfs, inode 256 is the root of a subvolume. This is the one check that needs no privileges.
  • .snapshots must appear as top level 5, a sibling of root, not a child of it.
  • The mount unit must exist under its escaped name, \x2esnapshots.mount — that exact name matters for grub-btrfs, see the note below.

Knock-on effect, if you also use grub-btrfs. The companion how-to Boot into Btrfs snapshots from GRUB documents a local override for grub-btrfs.path, needed because the packaged unit binds to a .snapshots.mount systemd unit that does not exist on the nested layout. With /.snapshots mounted from fstab that unit does exist, so the packaged path unit works unmodified and the override is unnecessary.

1c. Take a baseline snapshot

Terminal window
sudo snapper -c root create --description "baseline — fresh snapper install"

Verify:

Terminal window
sudo snapper list-configs
sudo snapper -c root list
sudo btrfs subvolume list / | grep '\.snapshots/'

You should see the root config listed and snapshot #1 (single, description “baseline…”). Snapshot #0 is the live system (“current”). The btrfs command should show .snapshots/1/snapshot as a child of the .snapshots subvolume.

Step 2 — Enable automatic timeline snapshots + cleanup

snapper ships three systemd timers, and there is a trap in how they end up enabled.

Immediately after dnf install snapper all three are disabled. But snapper create-config (Step 1a) enables snapper-timeline.timer by itself — on Fedora 44 / snapper 0.13.0-3 the symlink in /etc/systemd/system/timers.target.wants/ appears with the timestamp of the create-config run, not of any command you typed. It does not enable snapper-cleanup.timer.

The asymmetry is what matters: from the moment you create the config, hourly snapshots start accumulating and nothing prunes them. Enabling cleanup is therefore not optional housekeeping — it is what stops the growth you just started.

Terminal window
sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer

Naming snapper-timeline.timer too is harmless (enable is idempotent) and makes the command work regardless of the snapper version’s behaviour. Confirm both are enabled and active — list-timers alone would show a timer that was started but not enabled, and that one silently disappears at the next reboot:

Terminal window
systemctl is-enabled snapper-timeline.timer snapper-cleanup.timer
systemctl list-timers 'snapper-*' --no-pager

Both must print enabled.

  • snapper-timeline.timer → OnCalendar=hourly, creates a timeline snapshot every hour.
  • snapper-cleanup.timer → prunes old snapshots according to the retention limits in /etc/snapper/configs/root.

The stock Fedora retention is a sensible balance:

NUMBER_LIMIT="50" NUMBER_LIMIT_IMPORTANT="10" NUMBER_MIN_AGE="3600"
TIMELINE_LIMIT_HOURLY="10" TIMELINE_LIMIT_DAILY="10" TIMELINE_LIMIT_WEEKLY="0"
TIMELINE_LIMIT_MONTHLY="10" TIMELINE_LIMIT_QUARTERLY="0" TIMELINE_LIMIT_YEARLY="10"

Note TIMELINE_LIMIT_YEARLY="10": ten yearly snapshots are kept, so a handful of snapshots will pin whatever / looked like years ago. That is cheap on a system whose / barely changes, and expensive if something large and frequently rewritten lives inside root — see the /var/lib/flatpak note above. To adjust, edit the TIMELINE_LIMIT_* and NUMBER_LIMIT* values in /etc/snapper/configs/root.

There is also a snapper-boot.timer (a snapshot on every boot). It stays disabled — create-config does not touch this one. Leave it off unless you specifically want one.

Step 3 — Auto snapshot before/after every dnf transaction

3a. Install the actions plugin

Terminal window
sudo dnf install -y libdnf5-plugin-actions

This provides /usr/lib64/libdnf5/plugins/actions.so, which runs external commands on transaction hooks (pre_transaction, post_transaction, …). It is enabled by default once installed.

3b. Create the wrapper script

The script creates a linked snapper pre/post pair around each transaction, and derives the operation label (dnf install, dnf upgrade, dnf remove) by reading the dnf process’s command line. It is written so that every failure path exits 0 — a snapshot problem can never abort your dnf transaction.

Create /usr/local/bin/snapper-dnf-snapshot with this content:

#!/usr/bin/env bash
# Paired snapper pre/post snapshots around dnf5 transactions.
# Invoked by the libdnf5 "actions" plugin. Built to never abort a transaction:
# every failure path exits 0.
set -u
CONFIG=root
STATE=/run/snapper-dnf-pre-number
phase=${1:-}
dnfpid=${2:-$PPID}
command -v snapper >/dev/null 2>&1 || exit 0
snapper -c "$CONFIG" get-config >/dev/null 2>&1 || exit 0
# Build the snapshot description from the dnf command line (e.g. "dnf install").
describe() {
local desc="dnf" verb="" tok
if [ -n "$dnfpid" ] && [ -r "/proc/$dnfpid/cmdline" ]; then
while IFS= read -r -d '' tok; do
case "$tok" in
install|upgrade|update|upgrade-minimal|distro-sync|downgrade|reinstall|remove|erase|autoremove|swap|group)
verb="$tok"; break ;;
esac
done < "/proc/$dnfpid/cmdline"
fi
[ -n "$verb" ] && desc="dnf $verb"
printf '%s' "$desc"
}
case "$phase" in
pre)
num=$(snapper -c "$CONFIG" create --type pre \
--cleanup-algorithm number --description "$(describe)" --print-number) || exit 0
printf '%s\n' "$num" >"$STATE"
;;
post)
[ -r "$STATE" ] || exit 0
prenum=$(cat "$STATE"); rm -f "$STATE"
[ -n "$prenum" ] || exit 0
snapper -c "$CONFIG" create --type post --pre-number "$prenum" \
--cleanup-algorithm number --description "$(describe)" || exit 0
;;
esac
exit 0

Make it executable:

Terminal window
sudo chmod 0755 /usr/local/bin/snapper-dnf-snapshot

Paste tip: if your shell mangles multi-line here-documents on paste (bracketed-paste / autosuggest plugins can prepend stray characters to indented lines), don’t paste a sudo tee <<'EOF' … EOF block. Instead edit the file directly (sudoedit /usr/local/bin/snapper-dnf-snapshot), or write it to a normal file first and copy it into place with a single-line sudo install -m 0755 <file> /usr/local/bin/snapper-dnf-snapshot.

3c. Register the hooks

Actions files live in /etc/dnf/libdnf5-plugins/actions.d/ and must have a .actions extension. Each line is callback_name:package_filter:direction:options:command (empty fields between the colons mean “no filter / run once / default options”).

Create /etc/dnf/libdnf5-plugins/actions.d/snapper.actions:

# Snapper pre/post snapshot pair around every dnf transaction.
# Format: callback_name:package_filter:direction:options:command
pre_transaction::::/usr/local/bin/snapper-dnf-snapshot pre ${pid}
post_transaction::::/usr/local/bin/snapper-dnf-snapshot post ${pid}

${pid} is substituted by the plugin with the dnf process id, which the script reads to label the snapshot. The empty options field leaves raise_error at its default (log-only), so a failing hook is logged but never blocks the transaction.

Step 4 — Test

Install and remove a tiny package, then list the snapshots:

Terminal window
sudo dnf install -y hello && sudo dnf remove -y hello && sudo snapper -c root list | tail -n 6

You should see two correctly-linked pairs with operation labels, e.g.:

6 │ pre │ │ … │ root │ number │ dnf install
7 │ post │ 6 │ … │ root │ number │ dnf install
8 │ pre │ │ … │ root │ number │ dnf remove
9 │ post │ 8 │ … │ root │ number │ dnf remove

The post snapshot’s second column references its pre number (7→6, 9→8), and the description reflects the operation. From now on sudo dnf upgrade will show up as a dnf upgrade pair.

Step 5 — Optional: a second config for /home

Everything above protects the system. On Fedora’s layout /home is a separate subvolume, so none of it is covered — and that is usually the part people assumed was protected. A second config fixes that, and it is worth setting up differently from the root one.

Terminal window
sudo snapper -c home create-config /home

Then let your own account read its snapshots without sudo. This is the whole point of a home config: the common operation is “get yesterday’s version of a file back”, and needing root for that turns a 10-second recovery into a chore:

Terminal window
sudo snapper -c home set-config ALLOW_USERS="$USER" SYNC_ACL=yes

SYNC_ACL=yes is what makes ALLOW_USERS actually work — it puts an ACL on /home/.snapshots so the listed user can traverse it. Verify both halves:

Terminal window
snapper -c home list # no sudo — must work
snapper -c root list # no sudo — must print "No permissions."
getfacl -p /home/.snapshots | grep "^user:"

The ACL line should read user:<you>:r-x. From there, restoring a file is an ordinary copy:

Terminal window
ls /home/.snapshots/1/snapshot/$USER/
cp /home/.snapshots/<N>/snapshot/$USER/path/to/file ~/path/to/file

Three deliberate differences from the root config:

  • Leave /home/.snapshots nested. The top-level relocation in Step 1b exists to survive replacing the whole root subvolume during a rollback. For /home the real operation is file recovery, not subvolume replacement, so the extra machinery buys nothing. Nested is also automatically excluded from its own snapshots, being a subvolume boundary.
  • No restorecon needed. snapper creates /home/.snapshots in place, already labelled snapperd_data_t.
  • It inherits the timers you already enabled. create-config appends to SNAPPER_CONFIGS in /etc/sysconfig/snapper (it becomes "root home"), and the hourly timeline timer walks every config listed there. Hourly snapshots of /home start on their own — verify with sudo snapper -c home list an hour later.

Make the diffs usable: carve out the disposable directories

A stock /home is mostly not worth snapshotting. On the machine this was written on, /home held ~31 GB, of which barely 2 GB was irreplaceable (.config, .ssh, .gnupg, project working trees, Flatpak app data). The rest was IDE installs, language toolchains, an Android SDK and browser caches — all reconstructible.

On a large disk that is not a space problem. It is a signal problem: snapper -c home status A..B drowns in thousands of IDE index files, which is exactly what makes the tool useless for answering “what changed in my config yesterday?”.

Btrfs snapshots have no exclude patterns — the only mechanism is a subvolume boundary. So convert the worst offenders into nested subvolumes:

Terminal window
mv ~/.cache ~/.cache.old
btrfs subvolume create ~/.cache
chmod 700 ~/.cache
cp -a --reflink=auto ~/.cache.old/<anything-worth-keeping> ~/.cache/

Two warnings learned the hard way:

  • Nothing may be using the directory. An IDE runs its own binaries out of ~/.local/share/JetBrains; moving it while the IDE is open is not recoverable by undo. Close the applications first, or do it right after login.
  • Check for subvolumes already nested inside. If you followed the rclone how-to, ~/.cache/rclone-vfs is already a subvolume and may hold uploads that have not completed. Stop the mount (systemctl --user stop rclone@dropbox) before touching ~/.cache, recreate the subvolume in the new location, and copy its contents across rather than deleting them.

Do this after creating the config if you like — carving a subvolume out later simply removes it from future snapshots and leaves existing ones valid.

Using snapshots

  • List restore points:
    Terminal window
    sudo snapper -c root list
  • See what changed between two snapshots (e.g. a pre/post pair 6..7; use 0 for the live system):
    Terminal window
    sudo snapper -c root status 6..7
  • Restore individual files changed between two snapshots:
    Terminal window
    sudo snapper -c root undochange 6..7
  • Create a manual restore point before something risky:
    Terminal window
    sudo snapper -c root create -d "before nvidia driver change"
  • Full rollback of the whole system: see the next section — the obvious command does not do what its documentation suggests on Fedora.

undochange covers the large majority of real incidents: it reverts the files that changed between two snapshots, in place, with no reboot. Reach for a full rollback only when the system is too broken to fix file by file.

Rolling back the whole system

Why snapper rollback does nothing here

Every snapper guide ends with sudo snapper -c root rollback N. On a stock Fedora install that command is a no-op, and — worse for a recovery procedure — a silent one: it reports success, you reboot, and you are in exactly the state you tried to leave.

snapper rollback works by creating a writable snapshot and pointing the filesystem’s default subvolume at it (btrfs subvolume set-default). That only decides which subvolume gets mounted when nothing else specifies one. Fedora specifies one — twice:

Terminal window
grep ' / ' /etc/fstab # UUID=… / btrfs subvol=root,x-systemd.device-timeout=0 0 0
cat /proc/cmdline # … ro rootflags=subvol=root …

An explicit subvol= by name always wins over the default subvolume. Both the kernel command line and fstab pin the name root, so whatever you set as default is ignored at every boot. This is a property of the Fedora layout, not a snapper bug — snapper’s own manual notes that rollback “requires a properly configured system”, meaning the openSUSE layout where / is itself mounted from a snapshot.

Chasing the default subvolume is the wrong fix. Editing rootflags= out of the kernel command line is worse: it is easy to end up with an unbootable system, and it has to be redone on every kernel update.

The procedure that does work: swap the subvolume

Since the name root is what gets mounted, put the content you want under that name. Rename the current root out of the way, create a fresh root from the snapshot, reboot. Nothing in fstab, GRUB or the kernel command line changes — they keep pointing at subvol=root, which is now the restored state.

This is also why Step 1b matters: .snapshots is a sibling of root, so replacing root leaves the entire snapshot history — including the pre-rollback state — untouched.

Verified as far as a reboot allows. The steps below follow directly from the layout established above, but a full rollback-and-reboot cycle has not been re-run on the machine this how-to was written on. Read them before running them, and prefer the live-USB variant if the machine matters.

1. Pick the snapshot and note its number.

Terminal window
sudo snapper -c root list

Check that the snapshot is recent enough to contain your /etc/fstab including the .snapshots line from Step 1b. Restoring a snapshot taken before that change gives you back a system that no longer mounts /.snapshots:

Terminal window
sudo grep snapshots /.snapshots/<N>/snapshot/etc/fstab

2. Mount the top level of the filesystem.

Terminal window
sudo mkdir -p /mnt/btrfs-top
sudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-top
ls /mnt/btrfs-top # root, home, opt, .snapshots, ...

3. Swap root for the snapshot.

Terminal window
sudo mv /mnt/btrfs-top/root /mnt/btrfs-top/root.broken
sudo btrfs subvolume snapshot /mnt/btrfs-top/.snapshots/<N>/snapshot /mnt/btrfs-top/root
ls -d /mnt/btrfs-top/root /mnt/btrfs-top/root.broken

The snapshot under .snapshots/<N>/snapshot is read-only; btrfs subvolume snapshot without -r produces a writable copy, which is what a bootable / has to be.

4. Reboot immediately.

Terminal window
sudo systemctl reboot

Renaming the subvolume that is currently mounted as / is permitted — the running mount tracks it by id, not by name — but the running system’s idea of its own path is now stale. Do not linger, and do not run package transactions between the swap and the reboot. If you would rather not do this on a live system at all, boot a Fedora live USB, unlock LUKS (sudo cryptsetup open /dev/nvme0n1p3 luks-recover), mount subvolid=5, and run the same three commands there.

5. After the reboot, verify, then reclaim the space.

Terminal window
findmnt / # subvol=/root, as always
findmnt /.snapshots # still mounted
sudo snapper -c root list # history intact

Once you are satisfied the restored system is the one you wanted, delete the old root. Note that root.broken contains at least one nested subvolume (var/lib/machines on a stock install), and btrfs subvolume delete refuses to remove a subvolume that still has children — so list and remove those first:

Terminal window
sudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-top
sudo btrfs subvolume list -o /mnt/btrfs-top/root.broken
sudo btrfs subvolume delete /mnt/btrfs-top/root.broken/var/lib/machines
sudo btrfs subvolume delete /mnt/btrfs-top/root.broken
sudo umount /mnt/btrfs-top

Recent btrfs-progs also accept btrfs subvolume delete -R to do this recursively; check btrfs subvolume delete --help on your system rather than assuming it.

Keeping root.broken around for a day or two costs almost nothing (it shares its extents with the restored system) and gives you a way back if the rollback turned out to be the wrong call — swap it in exactly as above.

Notes and good practices

  • Snapshots are not a backup. They live on the same LUKS/Btrfs disk; a disk failure or a corrupt filesystem loses the snapshots too. Keep a separate, off-disk backup for real data protection.
  • dnf needs sudo. Only package changes (install/upgrade/remove) modify the system and trigger the hook. Read-only commands (dnf list, search, info, check-update, repoquery) need no sudo and create no snapshot.
  • The hook can’t break dnf. The actions plugin’s raise_error defaults to log-only, and the wrapper exits 0 on every failure path. Worst case, a transaction runs without a snapshot.
  • Ordering: the actions hook depends on the snapper root config existing (Step 1). Until it does, the script safely no-ops, so installing the hook is harmless either way.
  • --installroot caveat: the hook always snapshots the host root config. If you run dnf --installroot=… for containers/chroots and want to exclude those, add enabled=host-only in the options (4th) field of each actions line.
  • Steps 1–4 cover / only. /home needs its own config — see Step 5. Neither is a substitute for an off-disk backup.
  • A GUI is available, and it is packaged. btrfs-assistant is in Fedora’s own repos (no COPR): sudo dnf install btrfs-assistant. It browses and diffs snapper snapshots, restores files, and exposes btrfsmaintenance (periodic scrub/balance). It ships a polkit policy, so launching it from the desktop menu prompts for authentication rather than needing a root shell. Useful for browsing; the commands above remain the reliable path for anything structural.
  • snapper rollback is the one command not to trust here. It is in every other guide and it silently does nothing on Fedora — see Rolling back the whole system. If you have automation or notes that call it, fix them now rather than during an incident.
  • Booting into a snapshot (choosing a snapshot from the GRUB menu) needs grub-btrfs, which is not in Fedora’s repos and comes from a COPR. It is covered in the companion how-to: Boot into Btrfs snapshots from GRUB on Fedora. Read its Step 3 with Step 1b above in mind — the .snapshots.mount override it describes applies to the nested layout, not to the top-level one set up here.
  • /boot is not snapshotted. On a default Fedora install /boot is a separate ext4 partition, so kernels and initramfs images live outside every snapshot. Rolling back across a kernel update restores /usr/lib/modules without restoring the matching kernel image. For a kernel that won’t boot, GRUB’s “Advanced options for Fedora Linux” is the right tool, not a snapshot.

Quick reference

ActionCommand
Check root FS is Btrfsfindmnt -no FSTYPE /
See the real subvolume layoutsudo btrfs subvolume list /
Install snappersudo dnf install -y snapper
Create root configsudo snapper -c root create-config /
Move .snapshots to top levelsudo btrfs subvolume delete /.snapshots, recreate it under a subvolid=5 mount, add an fstab line (Step 1b)
Check .snapshots is top-levelfindmnt /.snapshots (a line = correct; no output = still nested)
Enable auto timeline + cleanupsudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer
Confirm timers survive a rebootsystemctl is-enabled snapper-timeline.timer snapper-cleanup.timer (both enabled)
Add a config for /homesudo snapper -c home create-config /home
Let your user read its snapshotssudo snapper -c home set-config ALLOW_USERS="$USER" SYNC_ACL=yes
Restore one file, no sudocp /home/.snapshots/<N>/snapshot/$USER/<path> ~/<path>
GUIsudo dnf install btrfs-assistant (Fedora repo, polkit-integrated)
Install dnf5 actions pluginsudo dnf install -y libdnf5-plugin-actions
List snapshotssudo snapper -c root list
Manual snapshotsudo snapper -c root create -d "reason"
Diff two snapshotssudo snapper -c root status A..B
Restore changed filessudo snapper -c root undochange A..B
Full rollbacknot snapper rollback — swap the subvolume, see Rolling back the whole system
Mount the Btrfs top levelsudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-top
List timerssystemctl list-timers 'snapper-*' --no-pager

Files created

PathPurpose
/etc/snapper/configs/rootsnapper config for the / subvolume
/etc/snapper/configs/homesnapper config for /home (Step 5, optional)
/etc/sysconfig/snapperSNAPPER_CONFIGS — the list the timers walk; create-config appends to it
/.snapshots/top-level Btrfs subvolume (sibling of root, not nested inside it) holding the snapshots, mounted via fstab
/home/.snapshots/nested subvolume holding the /home snapshots (Step 5)
/etc/fstabone added line mounting subvol=.snapshots at /.snapshots — back it up first
/usr/local/bin/snapper-dnf-snapshotwrapper that creates the pre/post pair and labels it
/etc/dnf/libdnf5-plugins/actions.d/snapper.actionsregisters the pre/post hooks with dnf5