Skip to content

Instantly share code, notes, and snippets.

@hasanabs
Last active August 19, 2026 22:29
Show Gist options
  • Select an option

  • Save hasanabs/817e2922e86f7222bbfe54869976dc47 to your computer and use it in GitHub Desktop.

Select an option

Save hasanabs/817e2922e86f7222bbfe54869976dc47 to your computer and use it in GitHub Desktop.

🎩 Fedora 44 + Btrfs + Snapper Tutorial

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/libvirt are isolated so rollbacks never touch them
  • Fedora 44 already ships with dracut, zram, Podman, and Btrfs support

🧭 OVERVIEW

You will:

  1. Install Fedora 44 with @-style Btrfs subvolumes
  2. Fix fstab, set default subvolume, regenerate GRUB and dracut
  3. Install and configure Snapper
  4. Hook Snapper into DNF5
  5. Install grub-btrfs
  6. Set up permanent rollback
  7. Install Btrfs Assistant
  8. Verify zram
  9. 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.


⚠️ IMPORTANT TRADE-OFF OF SEPARATE /boot

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.


⏱️ PHASE 1 — INSTALL FEDORA 44 WITH BTRFS SUBVOLUMES

📍 WHEN: First step — boot the Fedora installer

Download the Fedora 44 Workstation ISO from fedoraproject.org and boot it.


📐 Subvolume naming convention

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

🖥️ Scenario A — Empty Disk

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.


🪟 Scenario B — Dual Boot with Windows 11

⚠️ 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.


🗂️ Creating subvolumes in Anaconda

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 /):

  1. Click the / entry to select it in the left panel
  2. In the right panel, verify File System is set to Btrfs
  3. Find the Name (subvolume name) field — it may show something like fedora_root or root
  4. Change it to @

For each remaining subvolume:

  1. Click the + button at the bottom left
  2. Enter the Mount Point from the table (e.g. /home)
  3. Leave Desired Capacity blank to share space from the same Btrfs volume
  4. Click Add mount point
  5. In the right panel, confirm the Device Type is Btrfs Subvolume and it belongs to the same Btrfs volume as /
  6. Change the Name field to the @-prefixed name from the table (e.g. @home)
  7. 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 and noauto flags. 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 using btrfs 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.


⏱️ PHASE 2 — FIX FSTAB, REGENERATE GRUB AND DRACUT (Optional)

📍 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.

2.1 — View Anaconda's fstab

cat /etc/fstab

You 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.

2.2 — Get your UUIDs

sudo blkid

Note three values:

  • BTRFS_UUID — the UUID of your Btrfs root partition
  • BOOT_UUID — the UUID of your ext4 /boot partition
  • EFI_UUID — the UUID of your vfat /boot/efi partition

2.3 — Replace fstab

sudo nano /etc/fstab

Replace 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:1 vs compress=zstd: zstd without a level defaults to level 3 (higher compression, slightly more CPU). zstd:1 uses 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.btrfs is 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 pass 1.

2.4 — Ensure mount point directories exist

sudo mkdir -p /.snapshots

Other directories (/home, /var/log, /opt, etc.) already exist from the Fedora installation.

2.5 — Set @ as default Btrfs subvolume

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 @

2.6 — Regenerate GRUB and dracut

sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo dracut --force --regenerate-all

2.7 — Reboot

sudo reboot

2.8 — Verify mounts

After rebooting:

mount | grep " / "
mount | grep " /boot "
mount | grep " /boot/efi "
mount | grep "snapshots"
mount | grep "var_log"

Expected:

  • / → Btrfs subvol=/@
  • /boot → ext4
  • /boot/efi → vfat
  • /.snapshots → Btrfs subvol=/@snapshots
  • /var/log → Btrfs subvol=/@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

⏱️ PHASE 3 — INSTALL SNAPPER

sudo dnf install snapper -y
sudo snapper -c root create-config /

If needed, verify config exists:

cat /etc/snapper/configs/root

If it does not exist, recreate it:

sudo umount /.snapshots
sudo rm -rf /.snapshots
sudo snapper -c root create-config /
sudo mount -a

Tune:

sudo nano /etc/snapper/configs/root

Set:

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.timer

Apply 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

⏱️ PHASE 4 — CONNECT SNAPPER TO DNF5

⚠️ Fedora 44 uses DNF5. The package python3-dnf-plugin-snapper was 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.

4.0 — Install the actions plugin

sudo dnf install libdnf5-plugin-actions -y

If the package is already installed, this command is a no-op.

4.1 — Create the pre-transaction helper script

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-pre

4.2 — Create the post-transaction helper script

sudo 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-post

The 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. /run is cleared on every boot, so there is no persistent state.

4.3 — Create the actions file

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
EOF

Why helper scripts instead of inline commands? The libdnf5-plugin-actions plugin uses execv, 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.

4.4 — Test the hook

sudo dnf install cowsay -y
sudo snapper list

You 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.


⏱️ PHASE 5 — INSTALL GRUB-BTRFS

grub-btrfs adds a snapshot submenu to GRUB automatically whenever snapshots exist in /.snapshots.

5.1 — Install dependencies

sudo dnf install git make inotify-tools -y
awk --version
# Should show GNU Awk

5.2 — Install grub-btrfs

cd ~/Downloads
git clone https://github.com/Antynea/grub-btrfs.git
cd grub-btrfs
sudo make install

5.3 — Configure grub-btrfs for Fedora

sudo nano /etc/default/grub-btrfs/config

Set:

GRUB_BTRFS_GRUB_DIRNAME="/boot/grub2"
GRUB_BTRFS_MKCONFIG=/sbin/grub2-mkconfig
GRUB_BTRFS_SCRIPT_CHECK=grub2-script-check

5.4 — Regenerate GRUB

# 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.cfg

Verify snapshot block exists:

sudo awk '/BEGIN.*41_snapshots/,/END.*41_snapshots/' /boot/grub2/grub.cfg

Note: 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-run sudo grub2-mkconfig -o /boot/grub2/grub.cfg.

Note (VM users): During grub2-mkconfig, you may see a warning like mount: /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.service watches /.snapshots for changes and automatically regenerates GRUB whenever a snapshot is created or deleted — you never need to run grub2-mkconfig manually for snapshot updates.

Reboot and verify Fedora snapshots appears in GRUB.


⏱️ PHASE 6 — SET UP PERMANENT ROLLBACK

📍 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.

6.1 — Install the snapper-rollback script

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-rollback

All 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.


⏱️ PHASE 7 — INSTALL BTRFS ASSISTANT

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-assistant

⏱️ PHASE 8 — VERIFY ZRAM

zram 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
zramctl

⏱️ PHASE 9 — VERIFY PODMAN

Fedora already ships Podman:

podman --version
podman run hello-world

Note on container isolation: Podman images and container data live in /var/lib/containers, which is the @containers subvolume. 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 -y

🔄 ROLLBACK GUIDE

What 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 /boot still loads
  • For most package or config mistakes this is a complete rollback

There are three scenarios. Pick the one that matches your situation.


Scenario A — Normal rollback (system is running fine)

When: You are booted into your normal @ system and want to go back to a previous snapshot.

Step 1 — List snapshots:

sudo snapper list

Step 2 — Roll back:

sudo snapper-rollback <id>

Step 3 — Reboot:

sudo reboot

The 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 /mnt

Scenario B — You booted a snapshot to inspect it, and want to keep it

When: 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 reboot

Step 3 — Clean up the @.old-* backup once confirmed (same commands as Scenario A Step 4).


Scenario C — You accidentally modified a snapshot while booted into it

When: You booted a snapshot from GRUB and ran commands (installed packages, changed config) before realising you were inside a snapshot, not @.

What happened under the hood

  • 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's subvol=@ always wins — but it marks the snapshot with + in snapper list and 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.

Step 1 — Fix the stuck btrfs default subvolume (removes the + marker)

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.

Step 2 — Decide what to do with your current system state

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 reboot

After 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 /mnt

If your system won't boot at all

Boot 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
reboot

Boot 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.


🧠 FINAL STRUCTURE

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

🚨 COMMON MISTAKES

  • ❌ 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 1 in fstab — only /boot (ext4) gets 1
  • ❌ Setting ro false on a snapshot before attempting to boot it — try booting it read-only first, set ro false only 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 because subvol=@ in fstab overrides the Btrfs default subvolume; use snapper-rollback script 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-snapper on Fedora 44 — DNF4 plugin, has no effect on DNF5; use libdnf5-plugin-actions instead
  • ❌ Orphaned snapshots in @/.snapshots/ — if snapper ran before /.snapshots was mounted to @snapshots, snapshots were written inside @ instead of @snapshots. These appear in grub2-mkconfig output with @/.snapshots paths and must be manually deleted by mounting the Btrfs top-level with subvolid=5 and deleting each affected subvolume

✅ DONE

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 with subvol=@ fstab layout)

📋 QUICK REFERENCE

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



🔁 Bonus: reinstall the system without affecting user data

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.

During Installation

1. Use Custom or Blivet-GUI Partitioning

Reuse the existing Btrfs filesystem.

Do not recreate or reformat the Btrfs volume.

2. Create a New Root Subvolume

Create a new subvolume named:

@newroot

Assign:

/

3. Assign mountpoint manually

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

4. Complete Fedora Installation

Finish installation and boot successfully into the new system.

Verify:

findmnt /

Expected:

... subvol=@newroot

Rename @newroot to @ After Installation

After confirming the new installation works correctly:

1. Boot a Fedora Live ISO

Open a terminal.

2. Mount the Top-Level Btrfs Tree

Let assume your btrfs partition is /dev/nvme0n1p7

sudo mount -o subvolid=5 /dev/nvme0n1p7 /mnt

Verify:

sudo btrfs subvolume list /mnt

You should see both:

@
@newroot

3. Delete the Old Root

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.

4. Rename @newroot to @

sudo mv /mnt/@newroot /mnt/@

Verify:

sudo btrfs subvolume list /mnt | grep ' path @$'

Expected:

path @

5. Set @ as the Default Subvolume

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 /mnt

Verify:

sudo btrfs subvolume get-default /mnt

Expected:

path @

6. Update fstab

Check:

grep subvol /mnt/etc/fstab

If any entry references:

subvol=@newroot

replace it with:

subvol=@

7. Reinstall GRUB Configuration

Enter previlage first:

sudo -i

Mount 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; done

Enter chroot (root@localhost-live:~#bash-5.3#):

sudo chroot /mnt

Set @ 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-all

Regenerate GRUB:

grub2-mkconfig -o /boot/grub2/grub.cfg

Exit (bash-5.3#root@localhost-live:~#) and unmount:

exit
umount -R /mnt

8. Reboot

sudo reboot

Once you enter your system, verify:

findmnt /

Expected:

... subvol=@

The system is now running from @ while all user and application data subvolumes remain intact.

⚠️ Once we enter the system, re-do steps 3-7, and finally the system is ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment