Ubuntu The Hard Way

Updated On: 2026-04-27

Someone once asked how I setup Ubuntu the hard way with LUKS encrypted BTRFS subvolumes and TPM2 support.

Disclaimer: The Ubuntu installer is my recommended way of setting up Ubuntu until you want to dive into how it all works. If you're new to Linux, I recommend following along in a VM, or other low-risk environment.

I'm trying to showcase the commands and general approach here. Your milage may very.

Why Ubuntu without the installer:

  • More control over partitions
  • More control over the boot process from the start
  • Ability to make semi-standard or non-standard choices like systemd-boot and dracut.
  • For all the friends we meet along the way.

Intro And Setup

To get started, we'll setup our partition table. I use parted for this. I am going to use btrfs for the main OS, and fat32 for the efi+esp partition. Fat32 might be old, but it's required for UEFI boots. I consider my data more important than the ESP to boot into the system, so the plan is to create our esp partition at the end of the drive so that it can be resized or relocated later if needed.

Partitioning

Using Parted

I consider user data to be more important than boot-concerns, so if I choose to add other drives, I might choose to boot from there. As such, I place the ESP at the end of the device so that resizing data drives later overtop of it, or shrinking them to grow the space, is trivial. It will always be easier to recreate the ESP than a user's data.

parted -a optimal /dev/vda
mktable gpt
mkpart prevoe-esp -2048M 100%
set 1 esp on
mkpart prevoe-luks-0 0% -2048M
q

Encryption Setup

LUKS Formatting

Next we setup the data partition to use LUKS2. Boring, Simple, Predictable.

cryptsetup luksFormat --type=luks2 /dev/disk/by-partlabel/prevoe-luks-0

Decrypt the Disk

Decrypt the partition so that we can install into it. I choose to put btrfs into the decrypted partitions label so that I know at a glance what's what when recovering.

cryptsetup open /dev/disk/by-partlabel/prevoe-luks-0 prevoe-btrfs-0

Drive Formatting

Formatting the disk with btrfs is easy enough. Notably I'm setting the label here which matches the partition name. This means /dev/mapper/prevoe-btrfs-0 will now be the same as /dev/disk/by-label/prevoe-btrfs-0 after this step.

Format Devices

mkfs.btrfs --label prevoe-btrfs-0 /dev/mapper/prevoe-btrfs-0
mkfs.fat -F32 /dev/disk/by-partlabel/prevoe-esp

Mounts And Subvolumes

BTRFS Layout Setup

BTRFS has subvolumes, and we want to use them for the root of our OS, so we mount the root of the BTRFS partition and create the subvolumes of interest.

mkdir /mnt/btrfs-full
mount /dev/disk/by-label/prevoe-btrfs-0 /mnt/btrfs-full
cd /mnt/btrfs-full

# Now create each directory, and the neach volume
mkdir -p os/root/ubuntu-24.04
btrfs subvol create os/root/ubuntu-24.04/@live

mkdir -p home/catchall
btrfs subvol create home/catchall/@live

mkdir -p home/cprevoe
btrfs subvol create home/cprevoe/@live

OS Layout Setup Leveraging Subvolumes

We mount the subvolumes we've created and the esp so that we have the partition layout of our new OS available.

mkdir -p /mnt/realroot/
mount /dev/disk/by-label/prevoe-btrfs-0 -o subvol=/os/root/ubuntu-24.04/@live /mnt/realroot
mkdir -p /mnt/realroot/boot/efi /mnt/realroot/home
mount /dev/disk/by-label/prevoe-btrfs-0 -o subvol=/home/catchall/@live /mnt/realroot/home
mkdir -p /mnt/realroot/home/cprevoe
mount /dev/disk/by-label/prevoe-btrfs-0 -o subvol=/home/cprevoe/@live /mnt/realroot/home/cprevoe
mount /dev/disk/by-partlabel/prevoe-esp /mnt/realroot/boot/efi

OS Foundation Bootstrap

This is the heaviest step where we actually install everything which needs to be installed for a minimum system. For speed and network reasons, I'm using a local passthrough mirror. You can replace it with your ubuntu mirror of choice.

Install OS Base with Debootstrap

apt install debootstrap
debootstrap --arch=amd64 --variant=minbase noble /mnt/realroot \
  http://apt-cache-ng.k8s.andro.zdani.prevoe.com/archive.ubuntu.com/ubuntu/

OS Manual Configuration

The arch-install-scripts gives us access to genfstab and arch-chroot which are super useful.

Install arch-install-scripts

apt install arch-install-scripts

Create The fstab And cryptab

Next we generate our fstab, and then we generate our crypttab

mkdir -p /mnt/realroot/etc
genfstab -U /mnt/realroot > /mnt/realroot/etc/fstab

# We remove subvol references so we can swap out the subvolumes as desired
sed -i 's/subvolid=[0-9]*,//g' /mnt/realroot/etc/fstab

ROOT_UUID="$(blkid -s UUID -o value /dev/disk/by-partlabel/prevoe-luks-0)"
echo "prevoe-btrfs-0 UUID=${ROOT_UUID} none luks,tpm2-device=auto" > /mnt/realroot/etc/crypttab

Setup Basic Networking

Also to avoid errors, we set the hostname, and setup the expected hosts file.

echo prevoe-ubuntu > /mnt/realroot/etc/hostname
cat > /mnt/realroot/etc/hosts << EOF
127.0.0.1 localhost
127.0.1.1 prevoe-ubuntu
EOF

Kernel CMDLINE Generation Hook

Instead of grub, we're going to use systemd-boot, and dracut. With a bit of tinkering, one can setup the tpm to provide luks the key to automatically decrypt the drive.

Ubuntu seems to be heading in the direction of dracut producing the initrd, and systemd-ukify creating the universal kernal image (UKI). We can hook into this cleaner and in a more automated way than if we did it all in dracut, so here we go.

mkdir -p /mnt/realroot/etc/kernel/preinst.d
FILE=/mnt/realroot/etc/kernel/preinst.d/99-generate-kernel-cmdline
cat > ${FILE} << ENDOFSCRIPT
#!/bin/bash

# We will define our own cmdline so crypttab is used during boot
CMDLINE_FILE=/etc/kernel/cmdline

root_device=\$(awk '\$2 == "/" { print \$1 }' /etc/fstab)
root_device_options=\$(awk '\$2 == "/" { print \$4 }' /etc/fstab)

# If root device not in fstab, fallback to findmnt
if [ -z "\${root_device}" ]; then
    # Square brackets are possible in BTRFS
    root_device=\$(findmnt -no SOURCE / | cut -f1 -d'[')
    root_device_options=\$(findmnt -no OPTIONS /)
fi

[ -z "\${root_device}" -o -z "\${root_device_options}" ] && exit 0

cat > "\${CMDLINE_FILE}" << EOF
quiet splash
root=\${root_device}
rootflags=\${root_device_options}
rd.luks.crypttab=1
EOF
ENDOFSCRIPT

chmod +x ${FILE}

Kernel UKI Configuration

cat > /mnt/realroot/etc/kernel/install.conf << EOF
layout=uki
initrd_generator=dracut
uki_generator=ukify
EOF

Tell Dracut to Include crypttab

By default, the kernel now assumes that you either use gpt-auto-root or tell it where to find root with kernel command line arguments. This is great except our root is behind luks. Rather than tell the kernel via cmdline how to access it, I much prefer to embed that configuration instead.

So, we tell dracut to include the crypttab here. We also include tpm2-tss so it can decrypt using the TPM.

mkdir -p /mnt/realroot/etc/dracut.conf.d
cat > /mnt/realroot/etc/dracut.conf.d/99-include-crypttab.conf << EOF
# Required for automatic unlocking with tpm2
add_dracutmodules+=" tpm2-tss "

# Required for decrypting with rd.luks.crypttab=1
install_items+=" /etc/crypttab "
EOF

Configure Apt Sourcs

An old-fashioned sources.list was created for us in the old format by debootstrap, so we want to get rid of that as well and modernize.

echo "" > /mnt/realroot/etc/apt/sources.list

cat > /mnt/realroot/etc/apt/sources.list.d/ubuntu.sources << EOF
Types: deb
URIs: http://apt-cache-ng.k8s.andro.zdani.prevoe.com/archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

## Ubuntu security updates. Aside from URIs and Suites,
## this should mirror your choices in the previous section.
Types: deb
URIs: http://apt-cache-ng.k8s.andro.zdani.prevoe.com/security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main universe restricted multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF

OS Installation

We now have a basic skeleton of the OS, but it will not boot nor do anything useful yet. We chroot into this directory which allows us to interact with it as though it were the booted OS. We use arch-chroot from arch-install-scripts because it mounts all of the extras like /dev /proc and others for us.

Pivot Into the new OS

arch-chroot /mnt/realroot

Update and Install Non-Kernel Packages

And finally we have the installation. This is the heaviest part involving downloading a gig or two of packages and installing them all. The DEBIAN_FRONTEND prefix tells it to not pause to ask us for our opinions.

export DEBIAN_FRONTEND=noninteractive 
apt update \
  && apt upgrade -yq \
  && apt install -yq \
     ubuntu-desktop-minimal \
     ubuntu-desktop \
     sudo \
     dracut \
     systemd-boot \
     systemd-ukify \
     btrfs-progs \
     tpm2-tools \
     gawk

Install Kernel Packages

Unfortunately the ubuntu linux images in 24.04 have grub as a "recommends" which causes it to be installed. To avoid this, we separate our desktop install from our kernel install and install the latter with no-recommends.

# Leaving this in for people who pause
export DEBIAN_FRONTEND=noninteractive 

# We have to use no-install-recommends to avoid grub
apt install --no-install-recommends -yq \
  linux-generic-hwe-24.04

Install systemd-boot into ESP

Next we install the bootloader with bootctl. This allows our "Linux System Loader" to appear in our UEFI menu.

bootctl install && bootctl list

Add Users

Now if you know your users, it's time to set them up.

Setup Users

chown -R 2202:2202 /home/cprevoe
addgroup --gid 2202 cprevoe
adduser --uid 2202 --gid 2202 --gecos "Christopher Prevoe,,,,prevoe.com" cprevoe
adduser cprevoe sudo

Reboot And Test

And now we finish it off with a reboot. Exit out of the chroot, then restart the machine. Note, if you're following along using virsh, you might need to execute a virsh start directly.

Reboot!

reboot

Final Thoughts

And there we have it. Ubuntu 24.04 installed with BTRFS subvolumes used for critical components. You can now instantly snapshot the entire OS and even swap out the OS for other versions by mounting the btrfs-root and playing around in there.

And all this, just because someone once asked how I setup Ubuntu leveraging BTRFS subvolumes. And there we have it.

Hope this helps


© 2026 Christopher Prevoe.
All rights reserved.
prevoe.com