Partition and format the disk

This is the first chapter that writes to the target machine. Up to now everything lived in RAM and a power cycle undid it; from here on, the commands destroy whatever was on the disk before.

Unless a block says otherwise, every command in this chapter runs as root on the live system, over SSH.

What you are building

A single GPT disk with two partitions:

Partition Size Format Mounted at Holds
1 1 GiB FAT32 /boot The bootloader, kernel and initramfs
2 rest btrfs / etc. Everything else, optionally encrypted

The same two partitions serve both architectures, for different reasons. On an x86_64 PC the first one is the EFI System Partition that the firmware reads. On a Raspberry Pi 5 it is the firmware partition, where the Pi’s bootloader expects to find config.txt, the kernel and the device trees. Either way it is FAT32, one gigabyte, and mounted at /boot.

Everything else goes on btrfs, split into subvolumes. A subvolume is an independently mountable tree inside one filesystem: they all draw from the same pool of free space, so there is no partitioning decision to regret later, but each can be snapshotted, mounted, or rolled back on its own. That is what makes it possible, later, to roll back a bad update without taking your home directory with it.

This erases the disk

Everything below destroys all existing data on the disk you pick. There is no undo and no confirmation prompt beyond the ones you type yourself. Check the device name twice – on a machine you are operating blind, over SSH, from another room, it is worth being slower than feels necessary.

Choose the target disk

Run this on the live system:
lsblk -dpno NAME,SIZE,MODEL,TRAN,ROTA
(stdout)
/dev/sda       28.9G  USB 2.0 FD   usb   1
/dev/nvme0n1  238.5G  XP1000F256G  nvme  0

The last column is ROTA: 1 means rotational (a spinning disk, or in this case the USB stick you booted from), 0 means solid state. Find the disk you mean to install to, and be certain it is not the stick – that is the one entry you must never pick.

Set it once here, and every later block picks it up:

[zsh]: Set env vars
DISK=/dev/nvme0n1                                  # CHECK THIS -- it gets erased
case "$DISK" in *[0-9]) P=p;; *) P="";; esac       # p1/p2 or 1/2?
ESP="${DISK}${P}1"
ROOT="${DISK}${P}2"

That case line exists because disks disagree about how partitions are named. NVMe, SD and eMMC devices append p1, p2 to the device name; SATA and USB disks append 1, 2. Working it out once here means no later command in this chapter has to care which kind of disk you picked.

Check that all three came out the way you expect before continuing:

[bash]: Run this on your workstation:
echo "DISK=$DISK  ESP=$ESP  ROOT=$ROOT"
(stdout)
DISK=/dev/nvme0n1  ESP=/dev/nvme0n1p1  ROOT=/dev/nvme0n1p2
Shell variables live in one shell

DISK, ESP and ROOT are ordinary shell variables, so they exist only in the SSH session you set them in. If your connection drops – or you open a second terminal – set them again before running anything else in this chapter. A later block that silently sees an empty $DISK is exactly the kind of mistake this chapter cannot afford.

// settings
theme:
fx: