Manual Kubuntu installations

Reinstalling Kubuntu 26.04 on Encrypted ZFS Root

Recently I wrote about installing Kubuntu on ZFS. However, what to do if you already have a working setup and you just want reinstall OS?

In my case, I want to preserve my ZFS pool. Additionally, I want to retain all my ZFS datasets, except for the dataset that holds my file system. And yes, system is a bit peculiar but it’s nothing too special.

As one would assume, procedure is quite similar to the original installation. In order to fully understand why certain steps are in, I would suggest reading the original post. If you have a different setup, steps will differ, but the gist of it will remain the same.

As before, we start with root terminal.

sudo -i

Step to install the packages is also the same, as one would expect.

apt update
apt install -y gdisk zfsutils-linux

I setup the variables to help me to work.

DISK1=/dev/disk/by-id/<disk>
HOST=<host>
USERNAME=<user>

Since we don’t want to repartition disk, we can immediately skip to the step of fetching partition UUIDs.

DISK1P1=`blkid -s PARTUUID -o value $DISK1-part1`
DISK1P2=`blkid -s PARTUUID -o value $DISK1-part2`
DISK1P3=`blkid -s PARTUUID -o value $DISK1-part3`
DISK1P4=`blkid -s PARTUUID -o value $DISK1-part4`

As my setup is on top of LUKS, it is necessary to open the mapper.

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$DISK1P4 $DISK1P4

And now we can import the pool. Note that we will load it at /mnt/install so that it will match the original setup.

zpool import -N -R /mnt/install -f ${HOST^}

Now we can destroy our previous system. There is no coming back after this step.

zfs destroy ${HOST^}/System

Alternatively, if you want to keep it for reference, you can just rename it.

zfs rename ${HOST^}/System ${HOST^}/System.Old
zfs set mountpoint=/oldsys ${HOST^}/System.Old

With this in place, we get to create a new system dataset.

zfs create \
    -o devices=on \
    -o canmount=noauto -o mountpoint=/ \
    ${HOST^}/System
zfs mount ${HOST^}/System

Importantly, I also mount my home dataset so that my user stuff is there if I need it.

zfs mount ${HOST^}/Home

Lastly, we clean boot and EFI partition.

dd if=/dev/zero of=/dev/disk/by-partuuid/$DISK1P1 bs=1M
dd if=/dev/zero of=/dev/disk/by-partuuid/$DISK1P2 bs=1M

With all this in place, you can simply continue with the original guide at the step of formatting boot partition (mkfs.ext4). And that’s it - your OS will be as new.

Kubuntu 26.04 on Encrypted ZFS Root

My general destop choice is Kubuntu. I like Ubuntu package management and general compatibility and I love Plasma. I do have other distributions around my home, but my main setup sticks to Kubuntu. So, with the new version, came time to have a new setup instructions.

Honestly, installers these days are good and you will probably be happy with default installation unless you have special needs. In my case, I do have a few quirks the default installer doesn’t support to my liking:

  • ZFS for system and data
  • LUKS encryption for swap and ZFS
  • No Snap

Since installed doesn’t do that, I do quite a few manual steps to make it works. Mind you, everything is set for copy/paste so they are not that troublesome or excessive as they look. In any case, here they are.

The very first thing to do is to boot into installation. Once in live desktop I usually setup networking and add a few packages so I can acccess it from the network.

sudo apt update ; sudo apt install openssh-server
passwd

If you want to execute these commands directly, you can freely skip those steps. If you do want to access the system over network, do not forget to disable sleep as it will happily interrupt your installation steps.

Once in terminal, we want to switch to the root user.

sudo -i

Then we add a few prerequisite packages. Most notably ZFS and something to partition disk with (in my case sgdisk).

apt update
apt install -y gdisk zfsutils-linux

Now, as promised, I like to set a few variables.

DISK1=/dev/disk/by-id/<disk>
HOST=<host>
USERNAME=<user>

Note I use absolute path to the disk. I found it least problematic when scripting.

With that sorted out, we’re onto setting up partitions. My partitions are:

  • 127 MiB EFI partition
  • 1920 MiB boot partition
  • 32 GiB swap
  • rest is ZFS on top of 4K aligned LUKS

Note this will destroy any data you might have, thus there is no way back after this.

blkdiscard -f $DISK1 2>/dev/null
sgdisk --zap-all                              $DISK1
sgdisk         -n1:1M:+127M -t1:EF00 -c1:EFI  $DISK1
sgdisk         -n2:0:+1920M -t2:8300 -c2:Boot $DISK1
sgdisk         -n3:0:+32G   -t3:8200 -c3:Swap $DISK1
sgdisk -a 8 -I -n4:0:0      -t4:8309 -c4:LUKS $DISK1
sgdisk --print                                $DISK1

With partitions created, we can collect their IDs.

DISK1P1=`blkid -s PARTUUID -o value $DISK1-part1`
DISK1P2=`blkid -s PARTUUID -o value $DISK1-part2`
DISK1P3=`blkid -s PARTUUID -o value $DISK1-part3`
DISK1P4=`blkid -s PARTUUID -o value $DISK1-part4`

I’ve been going back and forth over the years on whether to use simpler partition names or UUIDs for the rest of setup. However, UUIDs are simply more fool-proof. For example, if you every clone your drive, it will just work. Partition names get too messy when you move disks around.

Now let’s setup LUKS encryption for ZFS.

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$DISK1P4

While your disk is encrypted, you still need to open it. I will assume you’re gonna use SSD, thus usage of perf-no_write_workqueue and perf-no_read_workqueue options.

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$DISK1P4 $DISK1P4

With this we can create our pool.

zpool create \
    -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    ${HOST^} /dev/mapper/$DISK1P4

I also recommend setting up quota but that’s something you can do later depending on how big the disk is.

On top of ZFS pool we can now create our system dataset.

zfs create \
    -o devices=on \
    -o canmount=noauto -o mountpoint=/ \
    ${HOST^}/System
zfs mount ${HOST^}/System

And a separate home dataset.

zfs create \
    -o canmount=noauto -o mountpoint=/home \
    ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home

Our boot partition wont be encrypted and that’s by design. It’s not ideal but recovering system with encrypted partition is such pain that I am willing to compromise.

yes | mkfs.ext4 /dev/disk/by-partuuid/$DISK1P2
mkdir /mnt/install/boot/
mount /dev/disk/by-partuuid/$DISK1P2 /mnt/install/boot/

Our EFI partition gets its FAT32 home too.

mkfs.msdos -F 32 -n EFI -i 4d65646f /dev/disk/by-partuuid/$DISK1P1
mkdir /mnt/install/boot/efi/
mount /dev/disk/by-partuuid/$DISK1P1 /mnt/install/boot/efi/

And lastly, our swap partition gets its own LUKS encrption. I strongly suggest the same password as for data partition as it will save you time typing.

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$DISK1P3

Mind you, it too needs opening.

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$DISK1P3 $DISK1P3

And formatting.

mkswap /dev/mapper/$DISK1P3

At this point I like to disable IPv6. I am not sure if it’s my network or a general issue, but IPv4-only network is MUCH faster in live USB environment. Mind you, this is just a temporary thing - installed system will support IPv6 just fine.

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

Finally, we get to install at least minimum Resolute Racoon.

apt update
apt dist-upgrade --yes
apt install --yes debootstrap
debootstrap --components=main,restricted,universe,multiverse resolute /mnt/install/

To help it going, I like to set its hostname and copy over network details. The last step is optional but it helps you preserver wireless network password.

echo $HOST > /mnt/install/etc/hostname
sed "s/kubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

At this time we switch into our new 26.04 environment. Anything you do after this step permanently impacts your installation. For all practical purposes, it’s like you’re logged in your new setup. Because you are.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys

chroot /mnt/install /usr/bin/env \
    DISK1P1=$DISK1P1 DISK1P2=$DISK1P2 DISK1P3=$DISK1P3 DISK1P4=$DISK1P4 \
    HOST=$HOST USERNAME=$USERNAME \
    bash --login

I like to setup my locale.

dpkg-reconfigure locales

And my timezone.

dpkg-reconfigure tzdata

With those out of way, it’s time to tell our system which partitions it will need to decrypt.

echo -n > /etc/crypttab
echo "$DISK1P3 PARTUUID=$DISK1P3 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "$DISK1P4 PARTUUID=$DISK1P4 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

Then we need to tell it where to mount what.

echo -n > /etc/fstab
echo "PARTUUID=$DISK1P2    /boot     ext4 noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$DISK1P1    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "/dev/mapper/$DISK1P4 none      auto nofail,nosuid,nodev,noauto                 0 0" >> /etc/fstab
echo "/dev/mapper/$DISK1P3 none      swap sw,nofail                                  0 0" >> /etc/fstab

Our new kernel packages go next.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

Followed by packages needed for making it boot.

apt install --yes zfs-initramfs cryptsetup keyutils plymouth-theme-spinner
update-initramfs -c -k all

To make EFI work, we need to install GRUB too. I modify its /etc/default/grub a bit to ensure it will resume from our swap. Note that you bootloader-id should stay ubuntu if you’re using secure boot.

apt install --yes grub-efi-amd64-signed shim-signed

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$DISK1P3)\"/" \
    /etc/default/grub
sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=1/'   /etc/default/grub
echo "GRUB_DISABLE_MEMTEST=true" >> /etc/default/grub

update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    --bootloader-id=Ubuntu --recheck --no-floppy

With booting sorted, it’s time to setup desktop and I like to start by forbidding snap altogether. Yes, there is a certail level of madness in using Ubuntu without snap but that’s story for another time.

apt remove --yes snapd 2>/dev/null
echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd
apt update

At last, we come to the desktop installation itself. I like to add a few tools here too.

apt install --yes kubuntu-desktop man-db lshw iw wget

Since disabling snap also disabled installation of Firefox, we need to readd it from PPA to have a browser installed once everything is done.

add-apt-repository --yes ppa:mozillateam/ppa
cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF
apt update && apt install --yes firefox

With desktop installed, we’re literally on the final stretch of completing install. So, it’s a good time to setup our user.

adduser --disabled-password --gecos '' $USERNAME
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USERNAME
echo "$USERNAME ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
passwd $USERNAME

Here I also like to setup my sleep options in order to enable hibernation.

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sed -i 's/.*HibernateMode=.*/HibernateMode=platform shutdown/'           /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

My preferred way of handling sleep on laptop is to suspend when closing the screen and hibernate on power key. If your preference differ, skip this step.

apt install -y pm-utils

sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

Since we’re good with our new environment we can exit it.

exit

Now we can disable devices flag on the system dataset and ensure all data is written. That is followed by unmounting all partitions and exporting ZFS pool.

zfs set devices=off ${HOST^}/System
sync

umount /mnt/install/boot/efi
umount /mnt/install/boot
mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}
zpool export -a

With cleanup done, we can finally reboot.

reboot

If everything went well, you now get to enjoy KDE Plasma.

Encrypted Kubuntu 24.10 with ZFS and Hibernation

For a while now I have published manual installation steps for pretty much any Ubuntu release since 18.10. Since, with Plasma 6, I switched to KDE and Kubuntu, I am abandoning that series. This guide will be the first one with manual installation steps for Kubuntu.

First, the big question: Why manual? Well, I view two things as absolutely mandatory for my Linux desktop: ZFS and encryption. One that I really like to have is hibernation. Default installation uses ZFS’ native encryption which, while nice and fast, doesn’t encrypt metadata. I am not saying that ZFS encryption is bad - I am just saying I am not necessarily comfortable with it. And swap size is way too small for hibernation to work. Thus, I like to do it all manually.

That said, if GUI installation leaves the system in state you like, there is no reason to follow this guide. Save yourself a bit of time.

As previously noted, this guide is for Kubuntu - an Ubuntu variant using KDE and not Gnome. While I was Gnome user for quite a long time, it was more of a stockholm syndrom rather than love. With KDE Plasma 6, I found something I actually like. As for the guide, Ubuntu and Kubuntu have a huge overlap and thus most of the steps are actually the same up to GUI install. It should be easy enough to look into an old Ubuntu guide and adjust the steps. I will probably still update Ubuntu steps for long-term releases (last one was 24.04)

With prologue done, what am I actually trying to achieve? Well, I want a minimal Kubuntu installation for my Framework 13 laptop that supports ZFS and hibernation. Both data and hibernation files are to be protected by LUKS encryption.

Finally, let’s go over all the steps to make it happen.

The first step is to boot into the USB installation and use “Try Ubuntu” option. Once on the a desktop, we want to open a terminal, and, since all further commands are going to need root access, we can start with that.

sudo -i

Then we add a few packets that come by default with Ubuntu install but are extras on Kubuntu.

apt update
apt install -y gdisk zfsutils-linux

Next step should be setting up a few variables - disk, hostname, and username. This way we can use them going forward and avoid accidental mistakes. Just make sure to replace these values with ones appropriate for your system.

DISK=/dev/disk/by-id/<disk>
HOST=<host>
USERNAME=<user>

For partition, I like to have 4 of them. The first two partitions are unencrypted and in charge of booting (boot + EFI). While I love encryption, I almost never encrypt the boot partitions in order to make my life easier as you cannot seamlessly integrate the boot partition password prompt with the later password prompt. Thus encrypted boot would require you to type the password twice (or thrice if you decide to use native ZFS encryption on top of that).

Third partition is swap that we need for hibernation support. I will use 64GB here because my laptop can have up to 96GB. While swap only requires about 40% of RAM to be backed by swap, having a few gigs extra will not hurt.

The last partition is the largest and will contain all user data.

One extra calculation is needed to figure out 4K aligned sector count.

LASTDISKSECTOR=$(( `blockdev --getsz $DISK` / 2048 * 2048 - 1 ))

And then we can create all the partitions needed:

blkdiscard -f $DISK 2>/dev/null
sgdisk --zap-all                               $DISK
sgdisk -n1:1M:+255M          -t1:EF00 -c1:EFI  $DISK
sgdisk -n2:0:+1792M          -t2:8300 -c2:Boot $DISK
sgdisk -n3:0:+64G            -t3:8200 -c3:Swap $DISK
sgdisk -n4:0:$LASTDISKSECTOR -t4:8309 -c4:LUKS $DISK
sgdisk --print                                 $DISK

To ease commands used later, here we can get partition UUIDs. While we can use disk names directly (as I often did before), using partition UUIDs helps if we ever clone disk to another physical drive.

PART1=`blkid -s PARTUUID -o value $DISK-part1`
PART2=`blkid -s PARTUUID -o value $DISK-part2`
PART3=`blkid -s PARTUUID -o value $DISK-part3`
PART4=`blkid -s PARTUUID -o value $DISK-part4`

The next step is to setup all LUKS partitions. If you paid attention, that means we need to repeat formatting a total of 2 times. Unless you want to deal with multiple password prompts, make sure to use the same password for both:

cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$PART4
cryptsetup luksFormat -q --type luks2 \
    --sector-size 4096 \
    --cipher aes-xts-plain64 --key-size 256 \
    --pbkdf argon2i /dev/disk/by-partuuid/$PART3

Since creating encrypted partitions doesn’t mount them, we do need this as a separate step. I like to name my LUKS devices based on partition names so we can recognize them more easily:

cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$PART4 $PART4
cryptsetup luksOpen \
    --persistent --allow-discards \
    --perf-no_write_workqueue --perf-no_read_workqueue \
    /dev/disk/by-partuuid/$PART3 $PART3

Finally, we can set up our ZFS pool with an optional step of setting quota to roughly 85% of disk capacity. Since we’re using LUKS, there’s no need to setup any ZFS keys. Name of the pool will match name of the host and it will contain several datasets to start with. Most of my stuff goes to either Data dataset for general use or to VirtualBox dataset for virtual machines. Consider this just a suggestion and a good starting point, adjust as needed.

zpool create \
    -o ashift=12 -o autotrim=on \
    -O compression=lz4 -O normalization=formD \
    -O acltype=posixacl -O xattr=sa -O dnodesize=auto -O atime=off \
    -O quota=3200G \
    -O canmount=off -O mountpoint=none -R /mnt/install \
    ${HOST^} /dev/mapper/$PART4
zfs create \
    -o reservation=100G \
    -o 28433:snapshot=72 \
    -o devices=on \
    -o canmount=noauto -o mountpoint=/ \
    ${HOST^}/System
zfs mount ${HOST^}/System
zfs create \
    -o 28433:snapshot=240 \
    -o canmount=noauto -o mountpoint=/home \
    ${HOST^}/Home
zfs mount ${HOST^}/Home
zfs set canmount=on ${HOST^}/Home
zfs create \
    -o 28433:snapshot=360 \
    -o canmount=noauto -o mountpoint=/Data \
    ${HOST^}/Data
zfs set canmount=on ${HOST^}/Data
zfs create \
    -o recordsize=32K \
    -o 28433:snapshot=72 \
    -o canmount=noauto -o mountpoint=/VirtualBox \
    ${HOST^}/VirtualBox
zfs set canmount=on ${HOST^}/VirtualBox
zfs set devices=off ${HOST^}

With ZFS done, we might as well setup boot, EFI, and swap partitions too:

yes | mkfs.ext4 /dev/disk/by-partuuid/$PART2
mkdir /mnt/install/boot/
mount /dev/disk/by-partuuid/$PART2 /mnt/install/boot/
mkfs.msdos -F 32 -n EFI -i 4d65646f /dev/disk/by-partuuid/$PART1
mkdir /mnt/install/boot/efi/
mount /dev/disk/by-partuuid/$PART1 /mnt/install/boot/efi/
mkswap /dev/mapper/$PART3

At this time, I also often disable IPv6 as I’ve noticed that on some misconfigured IPv6 networks it takes ages to download packages. This step is both temporary (i.e., IPv6 is disabled only during installation) and fully optional:

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1

To start the fun we need to debootstrap our OS. As of this step, you must be connected to the Internet:

apt update
apt dist-upgrade --yes
apt install --yes debootstrap
debootstrap --components=main,restricted,universe,multiverse \
    oracular /mnt/install/

We can use our live system to update a few files on our new installation:

echo $HOST > /mnt/install/etc/hostname
sed "s/kubuntu/$HOST/" /etc/hosts > /mnt/install/etc/hosts
cp /etc/netplan/*.yaml /mnt/install/etc/netplan/

At last, we’re ready to chroot into our new system.

mount --rbind /dev  /mnt/install/dev
mount --rbind /proc /mnt/install/proc
mount --rbind /sys  /mnt/install/sys
chroot /mnt/install /usr/bin/env \
    PART1=$PART1 PART2=$PART2 PART3=$PART3 PART4=$PART4 \
    HOST=$HOST USERNAME=$USERNAME USERID=$USERID \
    bash --login

With our newly installed system running, let’s not forget to set up locale and time zone:

locale-gen --purge "en_US.UTF-8"
update-locale LANG=en_US.UTF-8 LANGUAGE=en_US
dpkg-reconfigure --frontend noninteractive locales
dpkg-reconfigure tzdata

To download some stuff later, wget is useful.

apt install -y wget

In order for decryption to work, we do need to set up crypttab so our encrypted partitions:

echo -n > /etc/crypttab
echo "$PART3 PARTUUID=$PART3 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab
echo "$PART4 PARTUUID=$PART4 none luks,discard,initramfs,keyscript=decrypt_keyctl" >> /etc/crypttab

cat /etc/crypttab

To mount all those partitions, we also need some fstab entries too. ZFS entries are not strictly needed. I just like to add them in order to hide our LUKS encrypted ZFS from the file manager (habit from Nautilus days):

echo -n > /etc/fstab
echo "PARTUUID=$PART2    /boot     ext4 noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "PARTUUID=$PART1    /boot/efi vfat noatime,nofail,x-systemd.device-timeout=3s 0 1" >> /etc/fstab
echo "/dev/mapper/$PART3 none      swap sw,nofail                                  0 0" >> /etc/fstab
echo "/dev/mapper/$PART4 none      auto nofail,nosuid,nodev,noauto                 0 0" >> /etc/fstab

cat /etc/fstab

Now we’re ready to onboard the latest Linux kernel. Since this is not a LTS release, generic kernel will do.

apt update
apt install --yes --no-install-recommends linux-image-generic linux-headers-generic

On systems with a lot of RAM, I like to adjust memory settings a bit. This is inconsequential in the grand scheme of things, but I like to do it anyway. Think of it as wearing “lucky” socks:

echo "vm.swappiness=10" >> /etc/sysctl.conf
echo "vm.min_free_kbytes=1048576" >> /etc/sysctl.conf
cat /etc/sysctl.conf

Now we can create the boot environment:

apt install --yes zfs-initramfs cryptsetup keyutils plymouth-theme-spinner
update-initramfs -c -k all

And then, we can get grub going. Do note we also set up booting from swap (needed for hibernation) here too.

apt install --yes grub-efi-amd64-signed shim-signed

sed -i "s/^GRUB_CMDLINE_LINUX_DEFAULT.*/GRUB_CMDLINE_LINUX_DEFAULT=\"quiet splash \
    mem_sleep_default=deep \
    RESUME=UUID=$(blkid -s UUID -o value /dev/mapper/$PART3)\"/" \
    /etc/default/grub

update-grub
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
    --bootloader-id=Ubuntu --recheck --no-floppy

cat /boot/grub/grub.cfg  | grep 'linux' | grep 'ZFS' | head -1 | xargs | cut -d' ' -f3-

I don’t like snap so I preemptively banish it from ever being installed:

apt remove --yes snapd 2>/dev/null

echo 'Package: snapd'    > /etc/apt/preferences.d/snapd
echo 'Pin: release *'   >> /etc/apt/preferences.d/snapd
echo 'Pin-Priority: -1' >> /etc/apt/preferences.d/snapd

apt update

And now, finally, we can install our minimal desktop environment:

apt install --yes kde-plasma-desktop man-db

Since I have a framework laptop, I like to allow trim operation on its external USB drives.

cat << EOF | tee /etc/udev/rules.d/42-framework-storage.rules
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="13fe", ATTRS{idProduct}=="6500", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0005", ATTR{provisioning_mode}="unmap"
ACTION=="add|change", SUBSYSTEM=="scsi_disk", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0010", ATTR{provisioning_mode}="unmap"
EOF

In order to have hibernation to work properly, we need to disable a few devices. Fortunately, we can create script to do it for us every time system goes to sleep.

cat << EOF | tee /usr/lib/systemd/system-sleep/framework
#!/bin/sh
case \$1 in
  pre)
    for DRIVER_LINK in \$(find /sys/devices/ -name "driver" -print); do
      DEVICE_PATH=\$(dirname \$DRIVER_LINK)
      if [ ! -f "\$DEVICE_PATH/power/wakeup" ]; then continue; fi
      DRIVER=\$( basename \$(readlink -f \$DRIVER_LINK) )
      if [ "\$DRIVER" = "i2c_hid_acpi" ] || [ "\$DRIVER" = "xhci_hcd" ]; then
        echo disabled > \$DEVICE_PATH/power/wakeup
     fi
    done
  ;;
esac
EOF

sudo chmod +x /usr/lib/systemd/system-sleep/framework

For hibernation, I like to change sleep settings so that hibernation kicks in automatically after 13 minutes of sleep.

sed -i 's/.*AllowSuspend=.*/AllowSuspend=yes/'                           /etc/systemd/sleep.conf
sed -i 's/.*AllowHibernation=.*/AllowHibernation=yes/'                   /etc/systemd/sleep.conf
sed -i 's/.*AllowSuspendThenHibernate=.*/AllowSuspendThenHibernate=yes/' /etc/systemd/sleep.conf
sed -i 's/.*HibernateDelaySec=.*/HibernateDelaySec=13min/'               /etc/systemd/sleep.conf

For that we also need to do a minor lid switch configuration adjustment. I also like to set my power button as a hibernation trigger.

apt install -y pm-utils
sed -i 's/.*HandlePowerKey=.*/HandlePowerKey=hibernate/'                          /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitch=.*/HandleLidSwitch=suspend-then-hibernate/'           /etc/systemd/logind.conf
sed -i 's/.*HandleLidSwitchExternalPower=.*/HandleLidSwitchExternalPower=ignore/' /etc/systemd/logind.conf
sed -i 's/.*HoldoffTimeoutSec=.*/HoldoffTimeoutSec=13s/'                          /etc/systemd/logind.conf

Since Firefox is a snapd package (and we banished it), we can install it manually.

add-apt-repository --yes ppa:mozillateam/ppa

cat << 'EOF' | sed 's/^    //' | tee /etc/apt/preferences.d/mozillateamppa
    Package: firefox*
    Pin: release o=LP-PPA-mozillateam
    Pin-Priority: 501
EOF

apt update && apt install --yes firefox

Chrome aficionados, can install it too:

pushd /tmp
wget --inet4-only https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
apt install -y ./google-chrome-stable_current_amd64.deb
popd

Lastly, we need to have a user too.

adduser --disabled-password --gecos '' -u $USERID $USERNAME
usermod -a -G adm,cdrom,dialout,dip,lpadmin,plugdev,sudo,tty $USERNAME
echo "$USERNAME ALL=NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME
passwd $USERNAME

It took a while, but we can finally exit our debootstraped environment:

exit

Let’s clean all mounted partitions and get ZFS ready for next boot:

zfs set devices=off ${HOST^}/System

sync
umount /mnt/install/boot/efi
umount /mnt/install/boot
mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}
zpool export -a

After reboot, we should be done and our new system should boot with a password prompt.

reboot

Once system has booted, we can tell it to use local BIOS clock. It helps with dual-boot and I actually prefer to have local time in BIOS.

sudo timedatectl set-local-rtc 1 --adjust-system-clock

Of course, we can end with a hibernation test. If everything went correctly, this should work nicely.

sudo systemctl hibernate