Converting Pop!_OS 21.10 from ext4 to btrfs

· Updated · 8 min read · Linux

This guide converts a default, encrypted Pop!_OS 21.10 installation from ext4 to btrfs in place, without reinstalling. I wrote it as a note to self: my laptop was installed with the standard Pop!_OS procedure, which uses ext4, and I wanted btrfs snapshots without rebuilding the machine.

Written for Pop!_OS 21.10 in April 2022. Pop!_OS 21.10 was based on Ubuntu 21.10, which reached end of life on 14 July 2022; the steps are untested on later releases.

The procedure is based almost entirely on Willi Mutschler’s guide, Pop!_OS 21.10: installation guide with btrfs-LVM-luks and auto snapshots with BTRBK (accessed 30 March 2022). I tested it on VMs and a couple of live systems and documented the extra steps the conversion needs. Without his documentation, preparing this would have taken me far longer.

Back up your data before changing any filesystem. Data loss is a real risk here.

Why btrfs

I wanted snapshots, and btrfs integrates well with timeshift and timeshift-autosnap-apt. For background on the filesystem itself, see the btrfs documentation.

Assumptions

Pop!_OS was installed with the default encrypted layout: an EFI partition (FAT32), a recovery partition (FAT32), and a LUKS volume holding LVM with ext4 for / and swap. The procedure also worked on machines where I had moved swap inside the LUKS volume, which is what System76’s hibernation guide has you do.

The disk is /dev/sda throughout. Run lsblk or fdisk -l to check yours.

1. Prepare the installed system

As root (or with sudo), edit /boot/efi/loader/loader.conf and add timeout 2 at the end, so the boot menu stays up long enough to select the recovery partition. The file should end like this:

default Pop_OS-current
timeout 2

Install btrfs-progs, which Pop!_OS does not ship by default:

sudo apt install -y btrfs-progs

This regenerates the initramfs images and takes some time. Download the package file to the local filesystem as well. The recovery partition does not have btrfs-progs either, and a local copy saves you fetching it from there in step 2.

2. Check the filesystem from the recovery partition

Reboot into the recovery partition, open a terminal and become root:

sudo -i

Open the LUKS volume:

cryptsetup luksOpen /dev/sda3 cryptdata

Force a check on the original ext4 filesystem before converting it. Converting an inconsistent filesystem can easily lead to data loss.

Run:

fsck.ext4 -f /dev/mapper/data-root

Download btrfs-progs from pkgs.org or any other source, using amd64 as the architecture. If you saved the package earlier, mount that filesystem and install from there. At the time of writing the package is btrfs-progs_5.10.1-2build1_amd64.deb. Install it with dpkg:

dpkg -i btrfs-progs_5.10.1-2build1_amd64.deb

If you mounted your data-root volume to reach the package file, unmount it (this assumes it was mounted on /mnt):

cd /; umount /mnt

3. Convert and create the subvolumes

Convert / from ext4 to btrfs:

btrfs-convert /dev/mapper/data-root

On my laptop (Intel i5, 16 GB of RAM, SSD) the conversion took about 50 minutes for 230 GB in use. Allow for that.

Mount the converted volume. The ssd and discard options are for SSD and NVMe disks:

mount -o subvolid=5,ssd,noatime,space_cache,commit=120,compress=zstd,discard=async /dev/mapper/data-root /mnt

Create the @ subvolume for / and move everything into it:

btrfs subvolume create /mnt/@
# Create subvolume '/mnt/@'
cd /mnt
ls | grep -v @ | xargs mv -t @ #move all files and folders to /mnt/@
ls -a /mnt
# . .. @

Then create @home and move the contents of /home into it:

btrfs subvolume create /mnt/@home
# Create subvolume '/mnt/@home'
mv /mnt/@/home/* /mnt/@home/
ls -a /mnt/@/home
# . ..
ls -a /mnt/@home
# . .. diego
btrfs subvolume list /mnt

4. Update fstab

fstab must mount / from the @ subvolume, mount /home from the @home subvolume, and use btrfs mount options. Open it in an editor:

nano /mnt/@/etc/fstab

Get the UUID of the root volume and add the two lines below, substituting it:

blkid -s UUID -o value /dev/mapper/data-root
UUID=(id_from_the_above) / btrfs  defaults,subvol=@,ssd,noatime,space_cache,commit=120,compress=zstd,discard=async 0 0
UUID=(id_from_the_above) /home btrfs  defaults,subvol=@home,ssd,noatime,space_cache,commit=120,compress=zstd,discard=async 0 0

The file should end up like this:

cat /mnt/@/etc/fstab
# PARTUUID=6b533522-0c33-4f44-890f-4be275c5b06f  /boot/efi  vfat  umask=0077  0  0
# PARTUUID=45bb9da4-9571-40bc-8f20-468332234a62  /recovery  vfat  umask=0077  0  0
# /dev/mapper/cryptswap  none  swap  defaults  0  0
# UUID=591dae2e-37ce-42c9-8ceb-5b124658ca6a  /  btrfs  defaults,subvol=@,ssd,noatime,space_cache,commit=120,compress=zstd,discard=async  0  0
# UUID=591dae2e-37ce-42c9-8ceb-5b124658ca6a  /home  btrfs  defaults,subvol=@home,ssd,noatime,space_cache,commit=120,compress=zstd,discard=async   0 0

Your PARTUUID and UUID values will differ. The last two lines, for / and /home, are the ones that matter.

5. Update crypttab

Because the mount options include discard=async, add discard to crypttab:

sed -i 's/luks/luks,discard/' /mnt/@/etc/crypttab
cat /mnt/@/etc/crypttab
# cryptdata UUID=c5b8099a-f035-47fb-939f-fa4ea770a403 none luks,discard
# cryptswap UUID=52de8233-c50b-4873-b586-9ab313d28b56 /dev/urandom swap,plain,offset=1024,cipher=aes-xts-plain64,size=512

6. Update the kernelstub configuration

The systemd-boot settings must survive kernel and module updates, so add rootflags=subvol=@ to the "user" kernel options in the kernelstub configuration:

nano /mnt/@/etc/kernelstub/configuration

The file should look like this:

cat /mnt/@/etc/kernelstub/configuration
# {
#   "default": {
#     "kernel_options": [
#       "quiet",
#       "splash"
#     ],
#     "esp_path": "/boot/efi",
#     "setup_loader": false,
#     "manage_mode": false,
#     "force_update": false,
#     "live_mode": false,
#     "config_rev":3
#   },
#   "user": {
#     "kernel_options": [
#       "quiet",
#       "loglevel=0",
#       "systemd.show_status=false",
#       "splash",
#       "rootflags=subvol=@"
#     ],
#     "esp_path": "/boot/efi",
#     "setup_loader": true,
#     "manage_mode": true,
#     "force_update": false,
#     "live_mode": false,
#     "config_rev":3
#   }
# }

Add a comma after "splash" (on the line above your new "rootflags=subvol=@" option). Without it, update-initramfs fails later (see below).

7. Update the systemd-boot entry

Mount the EFI partition:

mount /dev/sda1 /mnt/@/boot/efi

Add rootflags=subvol=@ to the last line of Pop_OS-current.conf, either in a text editor or with:

sed -i 's/splash/splash rootflags=subvol=@/' /mnt/@/boot/efi/loader/entries/Pop_OS-current.conf
cat /mnt/@/boot/efi/loader/entries/Pop_OS-current.conf
# title Pop!_OS
# linux /EFI/Pop_OS-UUID_of_data-root/vmlinuz.efi
# initrd /EFI/Pop_OS-UUID_of_data-root/initrd.img
# options root=UUID=UUID_of_data-root ro quiet loglevel=0 systemd.show_status=false splash rootflags=subvol=@

UUID_of_data-root is the UUID of /dev/mapper/data-root. It is the value you got from blkid above, and you can also read it from fstab.

8. Chroot and rebuild the initramfs

A chroot lets you work inside the converted system without rebooting. Unmount the top-level filesystem from /mnt, then mount the @ subvolume there:

cd /
umount -l /mnt
mount -o defaults,subvol=@,ssd,noatime,space_cache,commit=120,compress=zstd,discard=async /dev/mapper/data-root /mnt

Enter the system:

for i in /dev /dev/pts /proc /sys /run; do mount -B $i /mnt$i; done
chroot /mnt

Check that fstab mounts everything correctly:

mount -av
# /boot/efi                : successfully mounted
# /recovery                : successfully mounted
# none                     : ignored
# /                        : ignored
# /home                    : successfully mounted

Update the initramfs so it picks up the changes:

update-initramfs -c -k all

If you get an error like this one:

update-initramfs: Generating /boot/initrd.img-5.11.0-7620-generic
kernelstub.Config    : INFO     Looking for configuration...
Traceback (most recent call last):
  File "/usr/bin/kernelstub", line 244, in <module>
    main()
  File "/usr/bin/kernelstub", line 241, in main
    kernelstub.main(args)
  File "/usr/lib/python3/dist-packages/kernelstub/application.py", line 142, in main
    config = Config.Config()
  File "/usr/lib/python3/dist-packages/kernelstub/config.py", line 50, in __init__
    self.config = self.load_config()
  File "/usr/lib/python3/dist-packages/kernelstub/config.py", line 60, in load_config
    self.config = json.load(config_file)
  File "/usr/lib/python3.9/json/__init__.py", line 293, in load
    return loads(fp.read(),
  File "/usr/lib/python3.9/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/usr/lib/python3.9/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/lib/python3.9/json/decoder.py", line 353, in raw_decode
    obj, end = self.scan_once(s, idx)
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 20 column 7 (char 363)
run-parts: /etc/initramfs/post-update.d//zz-kernelstub exited with return code 1

you most likely forgot the comma after "splash" in /etc/kernelstub/configuration (see step 6).

9. Reboot and check

Exit the chroot and reboot:

exit
reboot now

You should see a single passphrase prompt. Enter the LUKS passphrase and the system boots. If you got this far, the hard part is done. Click through the welcome screen, open a terminal and check the setup:

## check that everything is mounted correctly
sudo mount -av
# /boot/efi                : already mounted
# /recovery                : already mounted
# none                     : ignored
# /                        : ignored
# /home                    : already mounted
## check that root and /home are correctly mounted
sudo mount -v | grep /dev/mapper
# /dev/mapper/data-root on / type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache,commit=120,subvolid=265,subvol=/@)
# /dev/mapper/data-root on /home type btrfs (rw,noatime,compress=zstd:3,ssd,discard=async,space_cache,commit=120,subvolid=266,subvol=/@home)
##  check the swap partition
sudo swapon
# NAME      TYPE      SIZE USED PRIO
# /dev/dm-2 partition   4G   0B   -2
## show the btrfs filesystem on /
sudo btrfs filesystem show /
# Label: none  uuid: 591dae2e-37ce-42c9-8ceb-5b124658ca6a
#     Total devices 1 FS bytes used 8.15GiB
#     devid    1 size 468.43GiB used 10.02GiB path /dev/mapper/data-root
## check what subvolumes exist on  /
sudo btrfs subvolume list /
# ID 264 gen 82 top level 5 path ext2_saved
# ID 265 gen 82 top level 5 path @
# ID 266 gen 82 top level 5 path @home

Those checks follow Mutschler’s page. The optional steps below come from the man page of btrfs-convert:

## At this point you are done with the main steps and you can go on
## with the following optional steps:
## Optional but recommended steps taken from the man page of btrfs-convert:
## run defragmentation on the entire filesystem. 
## This will attempt to make file extents more contiguous.
sudo btrfs filesystem defrag -v -r -f -t 32M /
sudo btrfs filesystem defrag -v -r -f -t 32M /home
## this next steps is to  compact btrfs metadata. TAKES LONG!!
sudo btrfs balance start -m /
sudo btrfs balance start -m /home
## optionally delete the ext2 metadata
sudo btrfs subvolume delete /ext2_saved

10. SSD trim and maintenance

On an SSD or NVMe disk, enable fstrim.timer:

sudo systemctl enable fstrim.timer

Trimming an encrypted SSD also needs discard in crypttab (step 5). Check that /etc/lvm/lvm.conf contains issue_discards = 1, which is the default:

sudo grep "issue_discards" /etc/lvm/lvm.conf 
#     # Configuration option devices/issue_discards.
#     issue_discards = 1

I recommend installing btrfsmaintenance and configuring it to your needs in /etc/default/btrfsmaintenance:

sudo apt install -y btrfsmaintenance

If you want timeshift for snapshots, install timeshift and timeshift-autosnap-apt now. The conversion is complete.