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
- tags
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:
- Timeline — an automatic snapshot every hour (with auto-cleanup so they don’t pile up).
- Before/after every
dnftransaction — an automaticpre/postpair around each install / upgrade / remove. - 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
rootconfig snapshots therootsubvolume and nothing else — and Fedora’s installer splits a lot of the system into separate subvolumes. See What arootconfig actually snapshots. snapper rollbackdoes 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:
It must print
Terminal window findmnt -no FSTYPE /btrfs. (LUKS underneath is fine — this guide was written on a Btrfs-on-LUKS install.) If it printsext4orxfs, 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 namedroot. Inspect the full picture with: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.Terminal window sudo btrfs subvolume list /findmnt -t btrfs -o TARGET,SOURCE,OPTIONS 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):
| Subvolume | Mounted at | In a root snapshot? |
|---|---|---|
root | / | yes — this is what you are snapshotting |
home | /home | no |
opt | /opt | no |
cache | /var/cache | no |
log | /var/log | no |
spool | /var/spool | no |
tmp | /var/tmp | no |
containers | /var/lib/containers | no |
gdm | /var/lib/gdm | no |
libvirt | /var/lib/libvirt | no |
var/lib/machines | /var/lib/machines | no — 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:
/optis outside. Software installed there is not restored by a rollback. A package touching both/usrand/optwill be half-reverted:/usrgoes back,/optstays current. Rare on Fedora, but it is the one exclusion that can produce an inconsistent system rather than merely an unprotected one./homeis outside — deliberately. Restore points are for the system; user data belongs in a real backup. See the notes at the end for adding ahomeconfig if you want one anyway./var/lib/flatpakis inside. It is not a separate subvolume on a stock Fedora install, and it is easily several gigabytes. Check yours:EveryTerminal window findmnt /var/lib/flatpak # no output = not a separate subvolume, so it IS snapshottedsudo du -sh /var/lib/flatpakflatpak updatereplaces runtimes insideroot, 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/flatpakits own subvolume the way anaconda did forcontainersandlibvirt.
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
sudo dnf install -y snapperCreate a config named root for the / subvolume. This writes /etc/snapper/configs/root and creates a /.snapshots subvolume to hold the snapshots:
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:
sudo btrfs subvolume delete /.snapshotsFind the filesystem’s device node and UUID:
findmnt -no SOURCE / | sed 's/\[.*//' # → /dev/mapper/luks-<uuid>findmnt -no UUID / # → the Btrfs filesystem UUID for fstabMount the top level of the filesystem (subvolid=5 is the root of the Btrfs tree, above every named subvolume) and create .snapshots there:
sudo mkdir -p /mnt/btrfs-topsudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-topls /mnt/btrfs-top # root, home, opt, cache, log, ...sudo btrfs subvolume create /mnt/btrfs-top/.snapshotssudo umount /mnt/btrfs-topsudo rmdir /mnt/btrfs-topBack 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:
sudo cp -a /etc/fstab /etc/fstab.bak-snapperThen 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 0Then create the mount point, mount it, and fix the SELinux label:
sudo mkdir -p /.snapshotssudo chmod 750 /.snapshotssudo systemctl daemon-reloadsudo mount -asudo restorecon -Rv /.snapshotsThe 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:s0The 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:
sudo findmnt --verify --verboseIt 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:
findmnt /.snapshotsstat -c '%i' /.snapshotssudo btrfs subvolume list / | grep -E 'snapshots|top level 5'systemctl list-units --type=mount | grep -i snapshotsfindmntmust print a line withsubvol=/.snapshots— unlike the default layout, where it prints nothing.statmust print256: on Btrfs, inode 256 is the root of a subvolume. This is the one check that needs no privileges..snapshotsmust appear astop level 5, a sibling ofroot, 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.mountsystemd unit that does not exist on the nested layout. With/.snapshotsmounted from fstab that unit does exist, so the packaged path unit works unmodified and the override is unnecessary.
1c. Take a baseline snapshot
sudo snapper -c root create --description "baseline — fresh snapper install"Verify:
sudo snapper list-configssudo snapper -c root listsudo 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.
sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timerNaming 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:
systemctl is-enabled snapper-timeline.timer snapper-cleanup.timersystemctl list-timers 'snapper-*' --no-pagerBoth must print enabled.
snapper-timeline.timer→OnCalendar=hourly, creates atimelinesnapshot 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-configdoes 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
sudo dnf install -y libdnf5-plugin-actionsThis 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 -uCONFIG=rootSTATE=/run/snapper-dnf-pre-numberphase=${1:-}dnfpid=${2:-$PPID}
command -v snapper >/dev/null 2>&1 || exit 0snapper -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 ;;esacexit 0Make it executable:
sudo chmod 0755 /usr/local/bin/snapper-dnf-snapshotPaste 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' … EOFblock. 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-linesudo 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:commandpre_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:
sudo dnf install -y hello && sudo dnf remove -y hello && sudo snapper -c root list | tail -n 6You should see two correctly-linked pairs with operation labels, e.g.:
6 │ pre │ │ … │ root │ number │ dnf install7 │ post │ 6 │ … │ root │ number │ dnf install8 │ pre │ │ … │ root │ number │ dnf remove9 │ post │ 8 │ … │ root │ number │ dnf removeThe 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.
sudo snapper -c home create-config /homeThen 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:
sudo snapper -c home set-config ALLOW_USERS="$USER" SYNC_ACL=yesSYNC_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:
snapper -c home list # no sudo — must worksnapper -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:
ls /home/.snapshots/1/snapshot/$USER/cp /home/.snapshots/<N>/snapshot/$USER/path/to/file ~/path/to/fileThree deliberate differences from the root config:
- Leave
/home/.snapshotsnested. The top-level relocation in Step 1b exists to survive replacing the wholerootsubvolume during a rollback. For/homethe 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
restoreconneeded. snapper creates/home/.snapshotsin place, already labelledsnapperd_data_t. - It inherits the timers you already enabled.
create-configappends toSNAPPER_CONFIGSin/etc/sysconfig/snapper(it becomes"root home"), and the hourly timeline timer walks every config listed there. Hourly snapshots of/homestart on their own — verify withsudo snapper -c home listan 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:
mv ~/.cache ~/.cache.oldbtrfs subvolume create ~/.cachechmod 700 ~/.cachecp -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-vfsis 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; use0for 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:
grep ' / ' /etc/fstab # UUID=… / btrfs subvol=root,x-systemd.device-timeout=0 0 0cat /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.
sudo snapper -c root listCheck 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:
sudo grep snapshots /.snapshots/<N>/snapshot/etc/fstab2. Mount the top level of the filesystem.
sudo mkdir -p /mnt/btrfs-topsudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-topls /mnt/btrfs-top # root, home, opt, .snapshots, ...3. Swap root for the snapshot.
sudo mv /mnt/btrfs-top/root /mnt/btrfs-top/root.brokensudo btrfs subvolume snapshot /mnt/btrfs-top/.snapshots/<N>/snapshot /mnt/btrfs-top/rootls -d /mnt/btrfs-top/root /mnt/btrfs-top/root.brokenThe 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.
sudo systemctl rebootRenaming 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.
findmnt / # subvol=/root, as alwaysfindmnt /.snapshots # still mountedsudo snapper -c root list # history intactOnce 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:
sudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-topsudo btrfs subvolume list -o /mnt/btrfs-top/root.brokensudo btrfs subvolume delete /mnt/btrfs-top/root.broken/var/lib/machinessudo btrfs subvolume delete /mnt/btrfs-top/root.brokensudo umount /mnt/btrfs-topRecent 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.
dnfneedssudo. 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’sraise_errordefaults to log-only, and the wrapper exits0on every failure path. Worst case, a transaction runs without a snapshot. - Ordering: the actions hook depends on the snapper
rootconfig existing (Step 1). Until it does, the script safely no-ops, so installing the hook is harmless either way. --installrootcaveat: the hook always snapshots the hostrootconfig. If you rundnf --installroot=…for containers/chroots and want to exclude those, addenabled=host-onlyin theoptions(4th) field of each actions line.- Steps 1–4 cover
/only./homeneeds its own config — see Step 5. Neither is a substitute for an off-disk backup. - A GUI is available, and it is packaged.
btrfs-assistantis in Fedora’s own repos (no COPR):sudo dnf install btrfs-assistant. It browses and diffs snapper snapshots, restores files, and exposesbtrfsmaintenance(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 rollbackis 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.mountoverride it describes applies to the nested layout, not to the top-level one set up here. /bootis not snapshotted. On a default Fedora install/bootis a separate ext4 partition, so kernels and initramfs images live outside every snapshot. Rolling back across a kernel update restores/usr/lib/moduleswithout 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
| Action | Command |
|---|---|
| Check root FS is Btrfs | findmnt -no FSTYPE / |
| See the real subvolume layout | sudo btrfs subvolume list / |
| Install snapper | sudo dnf install -y snapper |
| Create root config | sudo snapper -c root create-config / |
Move .snapshots to top level | sudo btrfs subvolume delete /.snapshots, recreate it under a subvolid=5 mount, add an fstab line (Step 1b) |
Check .snapshots is top-level | findmnt /.snapshots (a line = correct; no output = still nested) |
| Enable auto timeline + cleanup | sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer |
| Confirm timers survive a reboot | systemctl is-enabled snapper-timeline.timer snapper-cleanup.timer (both enabled) |
Add a config for /home | sudo snapper -c home create-config /home |
| Let your user read its snapshots | sudo snapper -c home set-config ALLOW_USERS="$USER" SYNC_ACL=yes |
| Restore one file, no sudo | cp /home/.snapshots/<N>/snapshot/$USER/<path> ~/<path> |
| GUI | sudo dnf install btrfs-assistant (Fedora repo, polkit-integrated) |
| Install dnf5 actions plugin | sudo dnf install -y libdnf5-plugin-actions |
| List snapshots | sudo snapper -c root list |
| Manual snapshot | sudo snapper -c root create -d "reason" |
| Diff two snapshots | sudo snapper -c root status A..B |
| Restore changed files | sudo snapper -c root undochange A..B |
| Full rollback | not snapper rollback — swap the subvolume, see Rolling back the whole system |
| Mount the Btrfs top level | sudo mount -o subvolid=5 "$(findmnt -no SOURCE / | sed 's/\[.*//')" /mnt/btrfs-top |
| List timers | systemctl list-timers 'snapper-*' --no-pager |
Files created
| Path | Purpose |
|---|---|
/etc/snapper/configs/root | snapper config for the / subvolume |
/etc/snapper/configs/home | snapper config for /home (Step 5, optional) |
/etc/sysconfig/snapper | SNAPPER_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/fstab | one added line mounting subvol=.snapshots at /.snapshots — back it up first |
/usr/local/bin/snapper-dnf-snapshot | wrapper that creates the pre/post pair and labels it |
/etc/dnf/libdnf5-plugins/actions.d/snapper.actions | registers the pre/post hooks with dnf5 |