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