Btrfs snapshots with Snapper let you instantly undo system changes, failed updates, or accidental deletions without wasting disk space or needing separate backups for rollback. Fedora has used Btrfs as its default filesystem since version 33. In this tutorial, we're using Fedora 44.
This guide uses the recommended Fedora layout:
/boot/efi→ vfat/boot→ ext4/→ Btrfs
This is the safer, more Fedora-friendly setup for a general audience.
Key layout:
- Root uses Btrfs with a clean
@-style subvolume layout- Noisy areas like
/var/log,/var/cache,/tmp, and container storage are excluded from snapshots- Persistent data areas like
/home,/opt,/var/lib/flatpak,/var/lib/gdm, and/var/lib/libvirtare isolated so rollbacks never touch them- Fedora 44 already ships with dracut, zram, Podman, and Btrfs support
You will:
- Install Fedora 44 with
@-style Btrfs subvolumes - Fix fstab, set default subvolume, regenerate GRUB and dracut
- Install and configure Snapper
- Hook Snapper into DNF5
- Install grub-btrfs
- Set up permanent rollback
- Install Btrfs Assistant
- Verify zram
- Verify Podman
And...
🔁 Bonus (it may be needed in the future):
How to reinstall the system without affecting user data in /home, including installed software in flatpak, /opt, and others, even if they are in a single partition as Btrfs.
Because /boot is ext4 and outside Btrfs:
- ✅ More standard Fedora layout
- ✅ Lower GRUB complexity
- ✅ Lower risk for beginners
- ❌ Snapshots do not include kernel and initramfs
- ❌ Booting an old snapshot may still use the currently installed kernel from
/boot
This means:
Snapper rollback is still valid and useful, but snapshot boot from GRUB is best treated as a recovery/inspection tool, not a perfect full-system time machine.
📍 WHEN: First step — boot the Fedora installer
Download the Fedora 44 Workstation ISO from fedoraproject.org and boot it.
This tutorial creates 11 Btrfs subvolumes. The @ prefix is the convention used by Snapper, grub-btrfs, and the rollback script — they all look for subvolumes by these exact names.
| Subvolume | Mount point | Purpose |
|---|---|---|
@ |
/ |
Root — the only one rolled back by snapper-rollback |
@home |
/home |
User files — never rolled back |
@opt |
/opt |
Manually installed software — independent of rollbacks |
@flatpak |
/var/lib/flatpak |
Flatpak apps preserved across rollbacks |
@containers |
/var/lib/containers |
Podman images preserved across rollbacks |
@libvirt |
/var/lib/libvirt |
VM disk images — must never be rolled back with the system |
@var_log |
/var/log |
Logs persist across rollbacks and don't bloat snapshots |
@var_cache |
/var/cache |
Cache excluded from snapshots |
@tmp |
/tmp |
Ephemeral — no value in snapshotting |
@gdm or @sddm |
/var/lib/gdm or /var/lib/gdm |
GDM (for Gnome) or SDDM (for KDE) session state — rolling back this causes login failures |
@snapshots |
/.snapshots |
Snapshot storage — must be outside @ to survive rollbacks |
In the Anaconda installer:
- Go to Installation Method (2)
- On the top right corner, there is 3 dots, click and chose launch storage editor
- Chose the disk
- Create partition table
Create these partitions first:
| Partition | Size | Filesystem | Mount point | Notes |
|---|---|---|---|---|
| EFI | 600 MB | EFI System Partition (vfat) | /boot/efi |
UEFI boot |
| Boot | 1–2 GB | ext4 | /boot |
Fedora kernel/initramfs live here |
| Root | Remaining space | Btrfs | (no assigned) |
Main system + all subvolumes |
Then continue to Creating subvolumes in Anaconda below.
⚠️ Back up your data first.
Step 1 — Shrink Windows from inside Windows:
Open Disk Management → right-click C: → Shrink Volume → free at least 60–100 GB.
Then fully shut down Windows.
Step 2 — Install Fedora in the free space
In Anaconda:
- Go to Installation Destination
- Select your disk
- Choose Custom storage configuration
- Click Done
Create or assign:
| Partition | Size | Filesystem | Mount point | Notes |
|---|---|---|---|---|
| EFI | existing | existing vfat | /boot/efi |
❗ Reuse existing Windows EFI, do not format |
| Boot | 1–2 GB | ext4 | /boot |
New |
| Root | remaining free space | Btrfs | (no assigned) |
New |
❌ Do not format the Windows EFI partition.
Then continue to Creating subvolumes in Anaconda below.
After setting up your partitions in Scenario A or B, Anaconda's custom storage screen lists your mount points on the left. You now need to create each Btrfs subvolume before clicking Begin Installation.
For the root entry (which already exists as /):
- Click the
/entry to select it in the left panel - In the right panel, verify File System is set to
Btrfs - Find the Name (subvolume name) field — it may show something like
fedora_rootorroot - Change it to
@
For each remaining subvolume:
- Click the + button at the bottom left
- Enter the Mount Point from the table (e.g.
/home) - Leave Desired Capacity blank to share space from the same Btrfs volume
- Click Add mount point
- In the right panel, confirm the Device Type is
Btrfs Subvolumeand it belongs to the same Btrfs volume as/ - Change the Name field to the
@-prefixed name from the table (e.g.@home) - Repeat for each row in the subvolume table above
Note: Anaconda will write an fstab with non-standard formatting — entries may have
/mnt/sysroot/prefixes andnoautoflags. This is expected and harmless. You will replace the fstab in Phase 2. The system boots correctly regardless because dracut mounts the root filesystem directly from the kernel command line (rootflags=subvol=@), not from fstab.
Note: If Anaconda refuses to accept a mount point like
/var/lib/containers, skip it for now and add it manually in Phase 2 usingbtrfs subvolume create.
Complete the installation by going to manual disk mapping, mount point mapping. Then reboot into the installed Fedora. Do not start daily use — go to Phase 2 immediately.
📍 WHEN: First boot into the freshly installed Fedora, before any regular use
This is optional if we want to change the comperssion, realtime to noatime, etc.
cat /etc/fstabYou will see something like:
UUID=xxxx /mnt/sysroot/ btrfs subvol=@,compress=zstd:1,noauto 0 0
UUID=xxxx /mnt/sysroot/home btrfs subvol=@home,compress=zstd:1,noauto 0 0
...
The mount points are wrong (leading /mnt/sysroot/) and noauto prevents subvolumes from mounting at boot. Replace it entirely.
sudo blkidNote three values:
BTRFS_UUID— the UUID of your Btrfs root partitionBOOT_UUID— the UUID of your ext4/bootpartitionEFI_UUID— the UUID of your vfat/boot/efipartition
sudo nano /etc/fstabReplace the entire contents with the template below, substituting your actual UUIDs for BTRFS_UUID, BOOT_UUID, and EFI_UUID:
# <file system> <mount point> <type> <options> <dump> <pass>
# Btrfs subvolumes — pass 0 0 (Btrfs has built-in consistency checks, traditional fsck does not apply)
UUID=BTRFS_UUID / btrfs subvol=@,compress=zstd:1,noatime 0 0
UUID=BTRFS_UUID /home btrfs subvol=@home,compress=zstd:1,noatime 0 0
UUID=BTRFS_UUID /.snapshots btrfs subvol=@snapshots,noatime 0 0
UUID=BTRFS_UUID /var/log btrfs subvol=@var_log,noatime 0 0
UUID=BTRFS_UUID /var/cache btrfs subvol=@var_cache,noatime 0 0
UUID=BTRFS_UUID /tmp btrfs subvol=@tmp,noatime 0 0
UUID=BTRFS_UUID /var/lib/containers btrfs subvol=@containers,noatime 0 0
UUID=BTRFS_UUID /opt btrfs subvol=@opt,noatime 0 0
UUID=BTRFS_UUID /var/lib/flatpak btrfs subvol=@flatpak,noatime 0 0
UUID=BTRFS_UUID /var/lib/gdm btrfs subvol=@gdm,noatime 0 0
UUID=BTRFS_UUID /var/lib/libvirt btrfs subvol=@libvirt,noatime 0 0
# Standard partitions
UUID=BOOT_UUID /boot ext4 defaults 0 1
UUID=EFI_UUID /boot/efi vfat umask=0077 0 2
compress=zstd:1vscompress=zstd:zstdwithout a level defaults to level 3 (higher compression, slightly more CPU).zstd:1uses level 1 (faster, less compression). For a desktop root filesystem, level 1 is the better trade-off — meaningful space savings with near-zero overhead.
fsck pass numbers:
0 0→ all Btrfs subvolumes: Btrfs has its own internal consistency mechanism;fsck.btrfsis a stub that does nothing.0 1→/boot(ext4): checked first at boot.0 2→/boot/efi(vfat): checked after pass 1. Only one partition should ever have pass1.
sudo mkdir -p /.snapshotsOther directories (
/home,/var/log,/opt, etc.) already exist from the Fedora installation.
sudo btrfs subvolume set-default \
$(sudo btrfs subvolume list / | awk '/ path @$/{print $2}') /Verify:
sudo btrfs subvolume get-default /Expected output: ID <n> gen ... path @
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo dracut --force --regenerate-allsudo rebootAfter rebooting:
mount | grep " / "
mount | grep " /boot "
mount | grep " /boot/efi "
mount | grep "snapshots"
mount | grep "var_log"Expected:
/→ Btrfssubvol=/@/boot→ ext4/boot/efi→ vfat/.snapshots→ Btrfssubvol=/@snapshots/var/log→ Btrfssubvol=/@var_log
If any subvolume is missing from the mount output: the mount point directory may not exist inside
@. Create it and remount:sudo mkdir -p /path/to/mountpoint sudo mount -a
sudo dnf install snapper -y
sudo snapper -c root create-config /If needed, verify config exists:
cat /etc/snapper/configs/rootIf it does not exist, recreate it:
sudo umount /.snapshots
sudo rm -rf /.snapshots
sudo snapper -c root create-config /
sudo mount -aTune:
sudo nano /etc/snapper/configs/rootSet:
TIMELINE_CREATE="yes"
TIMELINE_LIMIT_HOURLY="3"
TIMELINE_LIMIT_DAILY="5"
TIMELINE_LIMIT_WEEKLY="3"
TIMELINE_LIMIT_MONTHLY="1"
TIMELINE_LIMIT_YEARLY="0"
NUMBER_LIMIT="5"
NUMBER_LIMIT_IMPORTANT="10"
EMPTY_PRE_POST_CLEANUP="yes"
Enable timers:
sudo systemctl enable --now snapper-timeline.timer
sudo systemctl enable --now snapper-cleanup.timerApply correct SELinux labels to the snapshots directory:
# restorecon is needed here because /.snapshots was created manually.
# Without it, SELinux may deny snapper access to the directory at runtime.
sudo restorecon -Rv /.snapshots❌ Do not create a snapper config for
/home.
Test:
sudo snapper create -d "test snapshot"
sudo snapper list
⚠️ Fedora 44 uses DNF5. The packagepython3-dnf-plugin-snapperwas written for DNF4 and does not hook into DNF5 transactions. Installing it will have no effect.
This phase uses libdnf5-plugin-actions — the native DNF5 plugin mechanism — to run Snapper before and after every package operation.
sudo dnf install libdnf5-plugin-actions -yIf the package is already installed, this command is a no-op.
sudo tee /usr/local/bin/snapper-dnf5-pre > /dev/null << 'EOF'
#!/bin/bash
CMD=$(tr '\0' ' ' < /proc/$PPID/cmdline 2>/dev/null | sed 's/[[:space:]]*$//')
snapper create --type pre --cleanup-algorithm number \
--print-number --description "${CMD:-unknown}" \
> /run/snapper-dnf-pre-number
EOF
sudo chmod +x /usr/local/bin/snapper-dnf5-presudo tee /usr/local/bin/snapper-dnf5-post > /dev/null << 'EOF'
#!/bin/bash
PRE=$(cat /run/snapper-dnf-pre-number 2>/dev/null)
[[ -z "$PRE" ]] && exit 0
CMD=$(tr '\0' ' ' < /proc/$PPID/cmdline 2>/dev/null | sed 's/[[:space:]]*$//')
snapper create --type post --cleanup-algorithm number \
--pre-number "$PRE" --description "${CMD:-unknown}"
rm -f /run/snapper-dnf-pre-number
EOF
sudo chmod +x /usr/local/bin/snapper-dnf5-postThe scripts communicate via
/run/snapper-dnf-pre-number— a tmpfs file that holds the pre-snapshot ID so the post script can pair with it./runis cleared on every boot, so there is no persistent state.
The plugin reads action files from /etc/dnf/libdnf5-plugins/actions.d/. Each line has five colon-separated fields: HOOK:PKG_FILTER:DIRECTION:OPTIONS:CMD.
sudo mkdir -p /etc/dnf/libdnf5-plugins/actions.d
sudo tee /etc/dnf/libdnf5-plugins/actions.d/snapper.actions > /dev/null << 'EOF'
pre_transaction::::/usr/local/bin/snapper-dnf5-pre
post_transaction::::/usr/local/bin/snapper-dnf5-post
EOFWhy helper scripts instead of inline commands? The
libdnf5-plugin-actionsplugin usesexecv, not a shell. You cannot use shell syntax, pipes, or redirections in the CMD field. The helper scripts handle the logic; the actions file just invokes them.
sudo dnf install cowsay -y
sudo snapper listYou should see a pre/post pair with descriptions dnf5 pre and dnf5 post. Every future sudo dnf install, upgrade, remove, etc. will automatically create a paired snapshot.
grub-btrfs adds a snapshot submenu to GRUB automatically whenever snapshots exist in /.snapshots.
sudo dnf install git make inotify-tools -y
awk --version
# Should show GNU Awkcd ~/Downloads
git clone https://github.com/Antynea/grub-btrfs.git
cd grub-btrfs
sudo make installsudo nano /etc/default/grub-btrfs/configSet:
GRUB_BTRFS_GRUB_DIRNAME="/boot/grub2"
GRUB_BTRFS_MKCONFIG=/sbin/grub2-mkconfig
GRUB_BTRFS_SCRIPT_CHECK=grub2-script-check
# Apply correct SELinux label to the grub-btrfs hook script
sudo restorecon -Rv /etc/grub.d/41_snapshots-btrfs
sudo grub2-mkconfig -o /boot/grub2/grub.cfgVerify snapshot block exists:
sudo awk '/BEGIN.*41_snapshots/,/END.*41_snapshots/' /boot/grub2/grub.cfgNote: If the output is empty here, it is not a bug. grub-btrfs only generates snapshot entries when snapshots already exist in
/.snapshots. The Phase 3 test snapshot should already be present — if you ran Phase 3 before this, re-runsudo grub2-mkconfig -o /boot/grub2/grub.cfg.
Note (VM users): During
grub2-mkconfig, you may see a warning likemount: /var/lib/os-prober/mount: fsconfig() failed. This is harmless — os-prober cannot access certain kernel namespaces inside a virtual machine. It does not affect GRUB or snapshot detection.
Enable daemon:
sudo systemctl enable --now grub-btrfsd.service
grub-btrfsd.servicewatches/.snapshotsfor changes and automatically regenerates GRUB whenever a snapshot is created or deleted — you never need to rungrub2-mkconfigmanually for snapshot updates.
Reboot and verify Fedora snapshots appears in GRUB.
📍 WHEN: After Phase 5
Snapper creates and tracks snapshots cleanly, but its built-in snapper rollback mechanism works by changing the Btrfs default subvolume. Because this tutorial's fstab mounts root with subvol=@ by name, that explicit name always wins at boot — the default-subvolume change has no visible effect.
The reliable approach for this layout is to replace the @ subvolume itself with a copy of the chosen snapshot. The script below does this safely: it renames the current @ to a timestamped backup, then creates a fresh @ from the snapshot.
sudo tee /usr/local/bin/snapper-rollback > /dev/null << 'EOF'
#!/bin/bash
set -e
[[ $EUID -ne 0 ]] && { echo "Run as root: sudo snapper-rollback <id>"; exit 1; }
SNAP_ID="${1:?Usage: sudo snapper-rollback <snapshot-id>}"
DEV=$(findmnt -n -o SOURCE / | sed 's/\[.*//')
TOPDIR=$(mktemp -d)
mount -o subvolid=5 "$DEV" "$TOPDIR"
trap "umount $TOPDIR 2>/dev/null; rmdir $TOPDIR 2>/dev/null" EXIT
SNAP_PATH="$TOPDIR/@snapshots/$SNAP_ID/snapshot"
[[ ! -d "$SNAP_PATH" ]] && { echo "ERROR: Snapshot #$SNAP_ID not found"; exit 1; }
BACKUP="@.old-$(date +%Y%m%d-%H%M%S)"
echo "Saving current @ as $BACKUP..."
mv "$TOPDIR/@" "$TOPDIR/$BACKUP"
echo "Creating new @ from snapshot #$SNAP_ID..."
btrfs subvolume snapshot "$SNAP_PATH" "$TOPDIR/@"
# btrfs subvolume snapshot always creates a writable destination.
# The ro false below is a safety guarantee in case this behaviour ever
# changes, but is redundant on current kernels.
btrfs property set "$TOPDIR/@" ro false
echo "Setting @ as default subvolume..."
NEW_DEFAULT=$(btrfs subvolume list "$TOPDIR" | awk '/ path @$/{print $2}')
btrfs subvolume set-default "$NEW_DEFAULT" "$TOPDIR"
echo ""
echo "Done. Reboot to apply:"
echo " sudo reboot"
echo ""
echo "Old @ kept as $BACKUP — delete it once satisfied:"
echo " sudo mount -o subvolid=5 $DEV /mnt"
echo " sudo btrfs subvolume delete /mnt/$BACKUP"
echo " sudo umount /mnt"
EOF
sudo chmod +x /usr/local/bin/snapper-rollbackAll rollback procedures — normal rollback, inspecting a snapshot safely, and recovering from accidental modifications while booted into a snapshot — are covered step by step in the Rollback Guide section below.
Btrfs Assistant is a graphical tool for managing your Btrfs filesystem. It lets you browse and restore snapshots, view subvolume usage, and monitor disk space — all without memorizing btrfs commands. Useful for day-to-day snapshot management.
sudo dnf install btrfs-assistant -y
btrfs-assistantzram is a compressed swap device that lives entirely in RAM. Instead of swapping to a slow disk, the kernel compresses idle memory pages and stores them in a reserved RAM region. This gives you effectively more usable RAM with near-zero latency and no wear on your drive. Fedora enables it by default — nothing to install.
swapon --show
zramctlFedora already ships Podman:
podman --version
podman run hello-worldNote on container isolation: Podman images and container data live in
/var/lib/containers, which is the@containerssubvolume. This means Podman data is intentionally preserved across root rollbacks — rolling back@never touches your container images. This is the expected and desired behaviour. If a rollback restores an older version of a containerised app, the container image itself remains at whatever version you last pulled.
If you want compose-like workflows:
sudo dnf install podman-compose -yWhat snapshots include on this layout:
- ✅
/etc,/usr, application state — your installed packages and config- ❌
/boot(kernel, initramfs) — separate ext4 partition, never snapshotted- ❌
/home,/opt,/var/lib/flatpak,/var/lib/gdm,/var/lib/libvirt— isolated subvolumes, intentionally excluded- Rolling back restores software state; the current kernel from
/bootstill loads- For most package or config mistakes this is a complete rollback
There are three scenarios. Pick the one that matches your situation.
When: You are booted into your normal @ system and want to go back to a previous snapshot.
Step 1 — List snapshots:
sudo snapper listStep 2 — Roll back:
sudo snapper-rollback <id>Step 3 — Reboot:
sudo rebootThe previous @ was saved as @.old-<timestamp> inside the Btrfs top-level as a safety net.
Step 4 — Verify, then clean up the backup:
Once you're satisfied the rollback is good:
DEV=$(findmnt -n -o SOURCE / | sed 's/\[.*//')
sudo mount -o subvolid=5 "$DEV" /mnt
sudo btrfs subvolume list /mnt # find the @.old-* name
sudo btrfs subvolume delete /mnt/@.old-<timestamp>
sudo umount /mntWhen: You booted a snapshot from GRUB to look at it, it looks good, and you want to make it permanent.
⚠️ Critical rule: make zero changes while booted into a snapshot. Writing to a snapshot subvolume permanently modifies it — it no longer represents that point in time. If you already changed something while inside the snapshot, go to Scenario C instead.
Step 1 — Reboot back to normal @:
At the GRUB menu, choose the normal Fedora Linux entry (not the snapshot entry). Verify you are on @:
btrfs subvolume get-default /
# Should show: path @Step 2 — Roll back from @ using the snapshot id you inspected:
sudo snapper-rollback <id>
sudo rebootStep 3 — Clean up the @.old-* backup once confirmed (same commands as Scenario A Step 4).
When: You booted a snapshot from GRUB and ran commands (installed packages, changed config) before realising you were inside a snapshot, not @.
snapper rollback(the built-in command, if used) changed the Btrfs default subvolume to point to your snapshot. On this layout that has no effect at boot —fstab'ssubvol=@always wins — but it marks the snapshot with+insnapper listand blocks deletion.snapper-rollback(the Phase 6 script) renamed old@to@.old-<timestamp>and created a new@from your snapshot — including any changes you made while inside it.
If snapper list shows a + on any snapshot, reset the default:
sudo btrfs subvolume set-default \
$(btrfs subvolume list / | awk '/ path @$/{print $2}') /Verify:
btrfs subvolume get-default /
# Should now show: path @Now sudo snapper delete <id> will work normally again.
Option 1 — Keep what you have (accidental changes are harmless)
Your current @ is the snapshot's state plus whatever you changed. If that's acceptable, continue using it and take a fresh snapshot as a new clean baseline:
sudo snapper create -d "post-rollback baseline"Option 2 — Restore the pre-rollback @ (undo everything)
The @.old-<timestamp> subvolume is your original @ from before the rollback — untouched. Restore it:
DEV=$(findmnt -n -o SOURCE / | sed 's/\[.*//')
sudo mount -o subvolid=5 "$DEV" /mnt
# Find the exact backup name
sudo btrfs subvolume list /mnt | grep '@.old'
# Save current tainted @, promote the backup
sudo mv /mnt/@ /mnt/@.tainted
sudo mv /mnt/@.old-<timestamp> /mnt/@
sudo umount /mnt
sudo rebootAfter rebooting you are back to exactly what you had before the rollback. Delete the tainted subvolume once confirmed:
DEV=$(findmnt -n -o SOURCE / | sed 's/\[.*//')
sudo mount -o subvolid=5 "$DEV" /mnt
sudo btrfs subvolume delete /mnt/@.tainted
sudo umount /mntBoot the Fedora Live USB, then attempt to boot the snapshot read-only first:
# Reboot → GRUB → Fedora snapshots
# Most snapshots will boot successfully in read-only mode for inspection.
#
# Only if the snapshot fails to boot (systemd errors during startup), set it writable:
sudo mount /dev/nvme0n1p6 /mnt
sudo btrfs property set /mnt/.snapshots/<id>/snapshot ro false
sudo umount /mnt
rebootBoot the snapshot from GRUB → Fedora snapshots. Once the system is up, reboot back to the normal Fedora entry, then follow Scenario B above to make it permanent.
| Path | Subvolume | Snapshotted | Reason |
|---|---|---|---|
/ |
@ |
✅ Yes | Main system recovery |
/home |
@home |
❌ No | User files — independent of system state |
/.snapshots |
@snapshots |
❌ No | Snapshot storage itself |
/var/log |
@var_log |
❌ No | Noisy, logs should persist across rollbacks |
/var/cache |
@var_cache |
❌ No | Noisy, cache rebuilds automatically |
/tmp |
@tmp |
❌ No | Ephemeral |
/var/lib/containers |
@containers |
❌ No | Container images survive rollbacks |
/opt |
@opt |
❌ No | Manually installed software — unrelated to system rollbacks |
/var/lib/flatpak |
@flatpak |
❌ No | Flatpak runtimes and apps — independent lifecycle |
/var/lib/gdm |
@gdm |
❌ No | GDM session state — rollback here causes login failures |
/var/lib/libvirt |
@libvirt |
❌ No | VM disk images — must never be rolled back with the system |
/boot |
— | ❌ No | Standard Fedora ext4 boot partition |
/boot/efi |
— | ❌ No | UEFI system partition |
- ❌ Expecting snapshots to include the kernel when using separate ext4
/boot - ❌ Formatting the Windows EFI partition during dual boot setup
- ❌ Not setting the default subvolume to
@ - ❌ Giving two partitions fsck pass
1in fstab — only/boot(ext4) gets1 - ❌ Setting
ro falseon a snapshot before attempting to boot it — try booting it read-only first, setro falseonly if it fails to start - ❌ Making changes (installing packages, editing files) while booted into a snapshot — modifies its historical state permanently
- ❌ Running
snapper rollback(built-in) on this layout — has no effect becausesubvol=@in fstab overrides the Btrfs default subvolume; usesnapper-rollbackscript instead - ❌ Forgetting Fedora-specific grub-btrfs settings (
GRUB_BTRFS_GRUB_DIRNAME,GRUB_BTRFS_MKCONFIG,GRUB_BTRFS_SCRIPT_CHECK) - ❌ Expecting GRUB snapshot submenu to appear before any snapshots exist
- ❌ Snapshotting container storage — leads to huge snapshots and unexpected data loss on rollback
- ❌ Using
python3-dnf-plugin-snapperon Fedora 44 — DNF4 plugin, has no effect on DNF5; uselibdnf5-plugin-actionsinstead - ❌ Orphaned snapshots in
@/.snapshots/— if snapper ran before/.snapshotswas mounted to@snapshots, snapshots were written inside@instead of@snapshots. These appear ingrub2-mkconfigoutput with@/.snapshotspaths and must be manually deleted by mounting the Btrfs top-level withsubvolid=5and deleting each affected subvolume
You now have:
- Standard Fedora boot layout:
/boot/efi+/boot+ Btrfs/ - Clean Btrfs subvolume layout with
@and proper isolation for system, user, VM, container, and app data — configured directly in Anaconda during installation - Snapper + DNF5 integration via
libdnf5-plugin-actions(native DNF5 hook, confirmed working on Fedora 44) - grub-btrfs snapshot submenu in GRUB with auto-regeneration daemon
- Permanent rollback via
snapper-rollback(compatible withsubvol=@fstab layout)
| Task | Command |
|---|---|
| List snapshots | sudo snapper list |
| Create manual snapshot | sudo snapper create -d "description" |
| Roll back to snapshot N | sudo snapper-rollback N && sudo reboot |
| View Btrfs subvolumes | sudo btrfs subvolume list / |
| Check current subvolume | btrfs subvolume get-default / |
| Check disk usage | sudo btrfs filesystem usage / |
| Regenerate GRUB manually | sudo grub2-mkconfig -o /boot/grub2/grub.cfg |
| Delete a snapshot | sudo snapper delete N |
| Open Btrfs Assistant | btrfs-assistant |
is procedure assumes:
- Existing Btrfs filesystem contains:
@home@flatpak@opt@containers@libvirt@var_log@var_cache@tmp@gdm (for Gnome) or @sddm (for KDE)@snapshots@
- Current root subvolume is
@(ID 282, far under subvolume@snapshots/13/snapshot), [after rollback to a snapshot]. Shown in this example:ID 257 gen 12993 top level 5 path @home ID 258 gen 12992 top level 5 path @flatpak ID 259 gen 12992 top level 5 path @opt ID 260 gen 12434 top level 5 path @containers ID 261 gen 12994 top level 5 path @libvirt ID 262 gen 7501 top level 5 path @var_log ID 263 gen 12970 top level 5 path @var_cache ID 264 gen 7880 top level 5 path @tmp ID 265 gen 12012 top level 5 path @gdm ID 266 gen 12013 top level 5 path @snapshots .... ID 281 gen XXX top level 258 path @snapshots/13/snapshot ID 282 gen HHH top level 5 path @ ID 283 gen YYY top level 258 path @snapshots/15/snapshot - Because of that, fedora installer (Anaconda) does not detect
@, durring manual assignment of mount point. - Goal is to reinstall Fedora while preserving all data subvolumes.
Reuse the existing Btrfs filesystem.
Do not recreate or reformat the Btrfs volume.
Create a new subvolume named:
@newroot
Assign:
/
Assign:
| Subvolume | Mount Point | Format needed |
|---|---|---|
@home |
/home |
- |
@opt |
/opt |
- |
@flatpak |
/var/lib/flatpak |
- |
@containers |
/var/lib/containers |
- |
@libvirt |
/var/lib/libvirt |
- |
@var_log |
/var/log |
recomended |
@var_cache |
/var/cache |
recomended |
@tmp |
/tmp |
recomended |
@gdm |
/var/lib/gdm |
- |
@snapshots |
/.snapshots |
yes |
For the @newroot, the installer will automatically format it
Finish installation and boot successfully into the new system.
Verify:
findmnt /Expected:
... subvol=@newroot
After confirming the new installation works correctly:
Open a terminal.
Let assume your btrfs partition is /dev/nvme0n1p7
sudo mount -o subvolid=5 /dev/nvme0n1p7 /mntVerify:
sudo btrfs subvolume list /mntYou should see both:
@
@newroot
Ensure the old root is no longer needed.
sudo btrfs subvolume delete /mnt/@Verify:
sudo btrfs subvolume list /mnt | grep ' path @$'No output should be returned.
sudo mv /mnt/@newroot /mnt/@Verify:
sudo btrfs subvolume list /mnt | grep ' path @$'Expected:
path @
Find the new subvolume ID:
sudo btrfs subvolume list /mnt | grep ' path @$'Example:
ID 350 path @
Set it as default:
sudo btrfs subvolume set-default 350 /mntVerify:
sudo btrfs subvolume get-default /mntExpected:
path @
Check:
grep subvol /mnt/etc/fstabIf any entry references:
subvol=@newroot
replace it with:
subvol=@
Enter previlage first:
sudo -iMount volumes (adjust X and Y according your assigned /boot and /boot/efi)
mount -o subvol=@ /dev/nvme0n1p7 /mnt
mount /dev/nvme0n1pX /mnt/boot
mount /dev/nvme0n1pY /mnt/boot/efi
for d in proc sys dev run; do mount --bind /$d /mnt/$d; doneEnter chroot (root@localhost-live:~# → bash-5.3#):
sudo chroot /mntSet @ as default Btrfs subvolume
btrfs subvolume set-default \
$(btrfs subvolume list / | awk '/ path @$/{print $2}') /Verify:
btrfs subvolume get-default /Regenerate initramfs:
dracut --force --regenerate-allRegenerate GRUB:
grub2-mkconfig -o /boot/grub2/grub.cfgExit (bash-5.3# → root@localhost-live:~#) and unmount:
exit
umount -R /mntsudo rebootOnce you enter your system, verify:
findmnt /Expected:
... subvol=@
The system is now running from @ while all user and application data subvolumes remain intact.