Converting Pop!_OS 21.10 from ext4 to btrfs
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.