Installing Debian with BTRFS, Snapper backups and GRUB-BTRFS

A. Nagatani 永谷
16 min readAug 5, 2023
Debian 12

In this text guide I will try to demonstrate an “expert install” mode of Debian, with it we will set up an encrypted BTRFS partition system and configure ~~Timeshift~~ Snapper to keep backups of our system and use GRUB-BTRFS to boot into our snapshot backups to recover our system when needed.

I started this guide for myself, as a way to document, study and understand the required steps to achieve this “expert installation”. My initial goal was to set a BTRFS system with Timeshift as my main tool for snapshots, however as I delved into this subject I realized that Timeshift although great, didn’t offer the features I desired the most, since then I have moved and changed this guide to use Snapper instead. The end goal is the same, set a new system with BTRFS, use Snapper to manage our snapshots and then have GRUB-BTRFS to allow us to boot directly into our snapshots without the need of a live media.

Let’s get started

Boot into your installing medium device, go to advanced settings and choose the “Expert Install” option. Graphic mode or not doesn’t matter.

expert install selection

Proceed with the installation choosing the options that better suits you, this is a pretty much standard installation until we hit the disk partitioning stage, if you are prompted to chose something that you are unsure about you can use the default settings, in this case the default setting already is the first highlighted one so you can just proceed with pressing “Enter”.

installation process

Set your language, country, locale and timezone. Configure your network, most of these steps you can just hit “enter” and the installer should do everything for you, if prompted to load additional components we won’t be needing any.

set users

When prompted to “Set users and password” do not allow login as root. This way our user will be automatically set in the “sudo” group.

manual partition

We will start to get to work once we hit the Partition manager. Once there chose the “manual partition” option.

Select your desired disk to create a new partition scheme, if your system is UEFI select to create a new “gpt” table, otherwise if a legacy boot system you should use “mbr”.

EFI partition
  • Create a new partition with the size of 512MB, this is more than enough to be our EFI partition.
boot partition
  • Create a new 1GB partition, format as ext4 and select the mount-point to /boot.
encrypted volume
  • Finally create a new partition with the whole space left available, select to use as an “physical volume for encryption”.

The main partitions are set.

Now select “Configure encrypted volumes” and hit “enter”. You will be asked to write the changes to disk, select “yes” and go on.

  • Select: “Create encrypted volumes”
  • Select the device to encrypt, should be the one you created as “physical volume for encryption”.
  • Hit “Finish”.
  • Now the installer will erase and overwrite the encrypted volume, if this is a new disk you can skip this step, otherwise let it go through, be aware that this process may take quite some time.
  • When its done the installer will ask you to set a new password for the encrypted volume, make sure to set a strong password.
set LVM

Now that we set our encrypted volume we must configure a new LVM inside of it. Select the “Configure the Logical Volume Manager” to start.

  • Write the changes to the disk.
  • Select “Create Volume Group”.
  • Give it a name, for the sake of simplicity I will name mine as “VG0”
  • Select the encrypted device for the new Volume Group, it should be something like: “/dev/mapper/sdX_crypt…”.
  • Write changes to the disk.
  • Now let’s create a single “Logical Volume” in the “VG0”. I will call it “LG0” and use all the space available.
  • Hit finish.
logical volume

Now under the device-mapper select our only Logical Volume to set the new BTRFS partition, and set the following options as showed bellow on the screenshot:

  • use as: BTRFS journaling file system
  • mount point: /
btrfs options

Now we can finish the partitioning:

final partition table example

When asked if would like to go back and create a SWAP partition select “NO”, we don’t need a SWAP partition on disk since we will be using ZRAM for SWAP.

Write the changes to the disk and go to the next step, but do not install the system yet!!

Before we Install

before we install

We still need to better set our BTRFS system and create the subvolumes we will be using.

Hit “alt+ctrl+F2” to enter the Busybox terminal. Hit enter.

Use the command “df” to see the current mounted file system:

df
df

Then we will need to unmount our target file system with the following commands:

umount /target/boot/efi/
umount /target/boot/
umount /target/
umount

Then we will mount our encrypted partition with:

mount /dev/mapper/$VOLUME_GROUP_NAME /mnt
mount
  • *Beware to replace “$VOLUME_GROUP_NAME” with the name of your volume_group created previously.

We will then change directory to the mounted system:

cd /mnt

Let’s see what we currently have inside our mounted partition with the “ls” command:

$ ls
@rootfs

You should see that we only have “@rootfs”, we will need to rename this file to only “@” to make it compatible with Timeshift. Use the command:

mv @rootfs/ @
renaming @rootfs

Now we can create additional BTRFS sub-volumes:

btrfs su cr @snapshots
btrfs su cr @home
btrfs su cr @log
btrfs su cr @cache
btrfs su cr @crash
btrfs su cr @tmp
btrfs su cr @spool
subvolumes

The idea of separating our main system in different subvolumes is to manage and save disk space, there is a lot of files that don’t need to be part of our system snapshots, the layout I’m using is just one of many suggested, you can use more or less subvolumes as you may see fit. Since I will be using virtual-machines in this system and I don’t want to keep virtual-machine images in my snapshot I will also create a subvolume just for these files, I will also create a subvolume for containers, I will not create but other subvolumes that you may want to consider to create is “opt” or Gnome related volumes such as “gdm3” and “AccountsService” in “/var/lib/”, Gnome needs theses subvolumes that contain user login info and must be always writable, if you try to boot a read-only Gnome snapshot without these subvolumes your system will hang before the login screen.:

btrfs su cr @images
btrfs su cr @containers
# GNOME related subvolumes
btrfs su cr @AccountsService
btrfs su cr @gdm

A detailed explanation of why use these subvolumes can be found here: https://sysguides.com/install-fedora-38-with-snapshot-and-rollback-support/

Now let’s mount our root sub-volume to be able to create the additional directories for our sub-volumes:

mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@ /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target
mount

Now create the mount-points for each sub-volume:

cd /target
mkdir -p .snapshots
mkdir -p home
mkdir -p var/log
mkdir -p var/cache
mkdir -p var/crash
mkdir -p var/tmp
mkdir -p var/spool
mkdir -p var/lib/libvirt/images
mkdir -p var/lib/containers
# GNOME related subvolumes mount-point
mkdir -p var/lib/AccountsService
mkdir -p var/lib/gdm3
# It’s also best to create /var/lib/portables and /var/lib/machines
# if not already there. Systemd creates them automatically
# as nested subvolumes. Nested subvolumes will force you to do some
# manual removal after restoring a snapshot and removing old snapshots.
mkdir -p var/lib/portables
mkdir -p var/lib/machines
example

Then mount each sub-volume to their mount-point:

mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@snapshots /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/.snapshots
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@home /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/home
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@log /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/log
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@cache /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/cache
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@crash /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/crash
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@tmp /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/tmp
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@spool /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/spool
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@images /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/lib/libvirt/images
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@containers /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/lib/containers
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@AccountsService /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/lib/AccountsService
mount -o noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@gdm /dev/mapper/$VOLUME_GROUP_NAME_FOR_ROOT /target/var/lib/gdm3
example

Don’t forget to also mount our “boot” and “efi” partitions:

mount /dev/sdX2 boot
mount /dev/sdX1 boot/efi
example

We are done with the partitioning, now we must edit our “file system table” to make these changes permanent.

To make this easier as possible pay attention to the line where “@rootfs” is, we will first edit this line to represent the changes we made after press “home” to go to the beginning of this line then use “ctrl+k” to cut and then “ctrl+u” to paste the same line 7 times. Then we can just edit the mount-point labels.

We will then edit the “fstab” file:

nano etc/fstab

The resulting “fstab” will look something like this:

/dev/mapper/$VOLUME_GROUP /                        btrfs  noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@                   0    0
/dev/mapper/$VOLUME_GROUP /.snapshots btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@snapshots 0 0
/dev/mapper/$VOLUME_GROUP /home btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@home 0 0
/dev/mapper/$VOLUME_GROUP /var/log btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@log 0 0
/dev/mapper/$VOLUME_GROUP /var/cache btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@cache 0 0
/dev/mapper/$VOLUME_GROUP /var/crash btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@crash 0 0
/dev/mapper/$VOLUME_GROUP /var/tmp btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@tmp 0 0
/dev/mapper/$VOLUME_GROUP /var/spool btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@spool 0 0
/dev/mapper/$VOLUME_GROUP /var/lib/libvirt/images btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@images 0 0
/dev/mapper/$VOLUME_GROUP /var/lib/containers btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@containers 0 0
/dev/mapper/$VOLUME_GROUP /var/lib/AccountsService btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@AccountsService 0 0
/dev/mapper/$VOLUME_GROUP /var/lib/gdm3 btrfs noatime,space_cache=v2,compress=zstd:1,ssd,discard=async,subvol=@gdm 0 0
fstab example

Now we need to move to the root directory and unmount “/mount”, after that we can exit the console with the “exit” command:

cd /
unmount /mnt
exit

Now we can go back to our standard installation, press “ctrl+alt+f1” and finally we can hit the option to install our base system.

Proceed with the default installation, chose the options that better suits you or you can just use the defaults.

These are the options I will be using:

  • Kernel to install: linux-image-amd64
  • Drivers to include in the initrd: generic
  • Scan extra installation media: No
  • network mirror: yes, http
  • Use non-free firmware: Yes
  • Use non-free software: Yes
  • Enable source repo: No
  • Services to use: security updates / releases updates / backported software
  • no automatic updates
tasksel
  • Software selection, I will select only the “standart system utilities”
  • Force GRUB installation to the EFI removable… : No
  • Update NVRAM: Yes
  • Run os-prober, chose yes if you have dual-boot with windows or more OS, no if Debian is the only OS.
  • Finish the installation
  • System clock set to UTC: No
  • Reboot: Yes

The first boot

first boot

By now we have our minimal installation done, just bare-bone Debian with no GUI, we will keep it this way for a little while until we create our fist snapshot, this first snapshot will be our “default point” so we can always revert to a clean install in case you want to try a different Desktop Environment, or in my case I’m doing this so in the future when a new stable release comes I can revert to a clean install and then upgrade to a newer version of Debian keeping things as straight forward as possible.

Since we didn’t create a dedicated swap partition let’s configure our ZRAM, install the package:

sudo apt update && sudo apt upgrade
sudo apt install zram-tools

By default the ZRAM swap will be 256MB, we can edit this in the config file:

sudo nano /etc/default/zramswap

Here’s the config I will be using:

# Compression algorithm selection
# speed: lz4 > zstd > lzo
# compression: zstd > lzo > lz4
# This is not inclusive of all that is available in latest kernels
# See /sys/block/zram0/comp_algorithm (when zram module is loaded) to see
# what is currently set and available for your kernel[1]
# [1] https://github.com/torvalds/linux/blob/master/Documentation/blockdev/zram.txt#L86
ALGO=lz4

# Specifies the amount of RAM that should be used for zram
# based on a percentage the total amount of available memory
# This takes precedence and overrides SIZE below
PERCENT=20

# Specifies a static amount of RAM that should be used for
# the ZRAM devices, this is in MiB
#SIZE=256

# Specifies the priority for the swap devices, see swapon(2)
# for more details. Higher number = higher priority
# This should probably be higher than hdd/ssd swaps.
#PRIORITY=100

Snapper

We will install Snapper and the required packages to set GRUB-BTRFS with:

sudo apt install snapper inotify-tools git make

We need to make some prep-work to set snapper to save our snapshots to the subvolume @snapshots that we created previously.

The default way that snapper works is to automatically create a new subvolume “.snapshots” under the path of the subvolume that we are creating a snapshot. Because we want to keep our snapshots separated from the backed up subvolume itself we must remove the snapper created “.snapshot” subvolume and then re-mount using the one that we created before in a separate subvolume at @snapshots.

We can do this by moving to the root directory, unmount the .snapshots directory and then remove it:

cd /
sudo umount .snapshots
sudo rm -r .snapshots

Now we can create a new configuration for Snapper with:

sudo snapper -c root create-config /

This new configuration should have created a new “.snapshots” directory and as well a new BTRFS subvolume of the same name. We will remove this new subvolume and link our own created @snapshots subvolume to this path, so our snapshots will be safely stored in a different location.

To remove the auto-created subvolume use:

sudo btrfs subvolume delete /.snapshots

Let’s re-create the directory:

sudo mkdir /.snapshots

Now we can re-mount our @snapshots to “/.snapshots” with:

sudo mount -av

Snapper is ready to be used, now we can start to set the configuration that we want for snapper. Snapper can automatically create new snapshots on a schedule, on every boot, before and after installing a new package.

For now I am not going to use any automatically created snapshots so I will set everything to manual.

Disabling auto-snapshot on boot:

sudo systemctl disable snapper-boot.timer

Disabling the Snapper timeline:

sudo snapper -c root set-config 'TIMELINE_CREATE=no'

Adding the “sudo” group to allow our user to use snapper:

sudo snapper -c root set-config 'ALLOW_GROUPS=sudo'
sudo snapper -c root set-config 'SYNC_ACL=yes'

This way snapper will only create snapshots when I manually create a new one or Snapper will also automatically create a pair of snapshots that are “pre” and “post” every time we use the “apt” command to install a new package. If you would like to change this behavior you can edit this file:

sudo nano /etc/apt/apt.conf.d/80snapper

I won’t change this however I will change the amount of snapshots kept, the cleanup algorithm for this kind of snapshots is of the type “number” and we can set the limit of snapshots that we want to keep, once this limit is surpassed Snapper will delete the extra snapshots. I will keep only 10 pairs of snapshots, you can set a different number if you desire, use the command to set this limit:

sudo snapper -c root set-config "NUMBER_LIMIT=10"
sudo snapper -c root set-config "NUMBER_LIMIT_IMPORTANT=10"

The cleanup algorithm runs once a day, you can edit this service if you wish to change that:

sudo nano /lib/systemd/system/snapper-timeline.timer

From the Arch-wiki: If you are using the provided systemd timers, you can edit them to change the snapshot and cleanup frequency.

For example, when editing the snapper-timeline.timer, add the following to make the frequency every five minutes, instead of hourly:

[Timer]
OnCalendar=
OnCalendar=*:0/5

When editing snapper-cleanup.timer, you need to change OnUnitActiveSec. To make cleanups occur every hour instead of every day, add:

[Timer]
OnUnitActiveSec=1h

Or if you wish you can also run the cleanup algorithm manually with:

sudo snapper cleanup number

Although I am not using the timeline snapshots, before we go on I will edit the config file to set the number of snapshots limits for the timeline in case we ever want to use the timeline we are already set:

sudo nano /etc/snapper/configs/root

And set this values:

TIMELINE_MIN_AGE="1800"
TIMELINE_LIMIT_HOURLY="5"
TIMELINE_LIMIT_DAILY="7"
TIMELINE_LIMIT_WEEKLY="0"
TIMELINE_LIMIT_MONTHLY="0"
TIMELINE_LIMIT_YEARLY="0"

Finally let’s create our first default snapshot, this will be a snapshot of type “single” and will be stored until we manually delete it. Use the command:

sudo snapper -c root create --description "default fresh install"

GRUB-BTRFS

Unfortunately GRUB-BTRFS is not available on the Debian repos so we are going to install from source, let’s clone the repo and install it:

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

The installation should be successful and if it is it should also already detect our created snapshot.

Now we must enable the monitoring service so that GRUB-BTRFS can automatically update every time we create or delete a snapshot and update our grub menu with:

# To start the daemon run:
sudo systemctl start grub-btrfsd
# To activate it during system startup, run:
sudo systemctl enable grub-btrfsd

We can now do some cleanup going back one parent directory and removing the cloned repo with:

cd ..
sudo rm -rf grub-btrfs/

Since we are using Snapper there isn’t anymore configuration that we need to do, if everything went well GRUB-BTRFS will recognize every time there is a change in the /.snapshots directory, you can always reach out to the GRUB-BTRFS documentation if you need to set anything different.

And congratulations, if you made this far, by now you have a complete BTRFS system with Snapper and GRUB-BTRFS all set and done. You can reboot now and see that there is a new entry on your grub menu that allows you to boot from your created snapshots.

Restoring the system from a snapshot

After you have set everything, now you should be able to create snapshots and boot directly from them from the grub menu.

In any case you wish to restore a past snapshot there are a few different ways to do so, you can do a “rollback”, this will completely recreate your system from a snapshot, and you can do “undochanges” this method compares the differences between two snapshots and them removes the changes.

Use this command to see the available snapshots:

sudo snapper ls

To undo changes use the command followed by the snapshot you want to change into and the snapshot you want to change from. So if you want to revert to snapshot number 4 and you are currently on snapshot number 9 the command would be:

sudo snapper undochanges 4..9

To make a rollback use the command:

sudo snapper --ambit classic rollbak <snapshot_number>
#rebbot your system
sudo reboot

If the rollback was successful reboot your system but on the grub menu do not boot from the default selection just yet.

On the grub menu navigate on the snapshots entry and boot from the latest snapshot available, this should be the new default system you just recovered, once you boot into this system open the terminal and run this command to make the changes permanent:

sudo update-grub

Now you can reboot and the default selection on the grub menu should be this newly restored system.

To confirm that you are on a read/write system run this command:

sudo btrfs prop get -ts /
# the output should be:
#ro=false

Conclusion

Now we have a minimal and fresh install of Debian 12 with snapper and grub-btrfs, you can create a snapshot of this state and keep it as a default snapshot so that you can always return to a fresh state in the case you want to restart or undo any changes in your system such as changing your Desktop Environment entirely.

This set up is not difficult per se however is considerably more prep work then just simply install and use Timeshift, however this set allow us way more control over our system and that is exactly what I wanted. If you have any questions or want to know more about it this subject at the end there is a lot of resources that I used to write this guide and useful documentations. Feel free to leave a question if you want and I will do my best to answer.

[EXTRA] Installing a minimal Desktop Environment

By now we everything set up with BTRFS, Snapper and GRUB-BTRFS but our system is bare bone, if you wish to install a minimal desktop environment you can use the following commands:

# Install XFCE
sudo apt install xfce4 xfce4-goodies

# Install minimal KDE
sudo apt install kde-plasma-desktop

# Install minimal GNOME
sudo apt install gnome-core

Until we meet again.

Resources used to write this article:

--

--