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.
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
lsblk -dpno NAME,SIZE,MODEL,TRAN,ROTA
/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:
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:
echo "DISK=$DISK ESP=$ESP ROOT=$ROOT"
DISK=/dev/nvme0n1 ESP=/dev/nvme0n1p1 ROOT=/dev/nvme0n1p2
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.