Install the system

The disk is partitioned, formatted and mounted under /mnt, and there is nothing on it. This step puts a complete Arch system there, makes it able to boot itself, and makes sure you can log in when it does.

Run this on the live system:
setup install
================ Review =================
Host:         myhost
Detected:     x86_64 PC, UEFI
Packages:     base btrfs-progs cryptsetup sudo openssh iwd vim git pacutils expect snapper snap-pac linux linux-firmware intel-ucode
              + packages.txt: just tmux
Boot:         systemd-boot
Units:        systemd-networkd systemd-resolved iwd sshd snapper-cleanup.timer
Accounts:     admin (1000, wheel, passwordless sudo), user
SSH keys:     3, from /root/arch/authorized_keys
Host repo:    /root/arch -> /mnt/home/admin/arch
Maintenance:  aconfmgr 96ab414 (admin diff/apply/save/upgrade), snapper + snap-pac
=========================================

? Install? (Y/n):

Everything on this screen was decided either by the hardware or by the host repo. If any of it is not what you want, answer n, change the host repo, and run it again.

The same screen on a Raspberry Pi

Three lines read differently on a Pi 5, because they are the three things chosen by the hardware:

Detected:     Raspberry Pi 5 (aarch64, bcm2712)
Packages:     ... snapper snap-pac linux-rpi-16k firmware-raspberrypi linux-firmware
Boot:         Pi firmware (cmdline.txt)

linux-rpi-16k and firmware-raspberrypi replace linux and the intel-ucode / amd-ucode microcode, and there is no bootloader to install. Everything else – the subvolumes, the accounts, the units, the host repo, the maintenance tools – is the same on both.

After you confirm, it works through eleven stages. Apart from the passwords near the end, it asks nothing more.

1. Packages

setup refreshes the live system’s pacman keyring, then installs the base set and everything in packages.txt into /mnt with pacstrap – pacman pointed at a different root. There is no image and no magic: when it finishes, /mnt is an ordinary directory tree that happens to be a Linux system.

The base set is chosen from the hardware:

Package Why it is there
base The minimum system: glibc, bash, coreutils, pacman, systemd
linux / linux-rpi-16k A kernel, which base does not include; picked by uname -m, and on aarch64 only for a bcm2712 board
linux-firmware Firmware blobs for WiFi, GPUs and other devices
firmware-raspberrypi Pi only: VideoCore firmware and the files its bootloader reads
intel-ucode / amd-ucode x86_64 only, by CPU vendor: microcode errata fixes, loaded at boot
btrfs-progs The root filesystem is btrfs and cannot be checked without it
cryptsetup Always: a couple of megabytes, and forgetting it on an encrypted disk is a far worse afternoon
sudo Administration from admin
openssh The whole point of a headless machine
iwd WiFi
vim An editor that exists inside the installed system
less git’s pager. Only an optional dependency of git, so without it git log in the host repo fails outright rather than merely not paging
git The host repo is a git repository
pacutils, expect What aconfmgr needs to compare the machine with the repo
snapper, snap-pac Snapshots of /, and the pacman hooks that take them

The keyring refresh matters more than it looks. Every package is checked against developer signing keys, and the copy of those keys on the stick is as old as the image; a stick that has been in a drawer for six months will eventually meet a package signed by a key it has never heard of. The resulting error names a signature rather than a stale keyring, which sends people down the wrong path.

pacstrap uses the live system’s mirror list, which by default starts with Arch’s own geo-routed CDN – a fine first choice anywhere. If you would rather draw from a measured-fastest mirror near you, run reflector --country US --protocol https --age 12 --sort rate --save /etc/pacman.d/mirrorlist before setup install (x86_64 only; reflector is not on the Pi image and knows nothing about Arch Linux ARM’s mirrors).

On an older Pi stick

Early BOOMSTICK Pi images shipped without Arch Linux ARM’s signing keys, and on one of those every ALARM package – including the kernel – fails as unknown trust. setup checks before it starts, and stops with the fix: rebuild the stick, or sign the key locally with pacman-key --lsign-key 68B3537F39A313B3E574D06777193F152BDBE6A6. On such a stick it also runs pacstrap without -K, because -K gives the target a fresh keyring and would leave that hand-made trust behind.

On a re-run, when the base system is already there, this stage only installs what is missing.

2. fstab

The new system needs to know how to mount its own filesystems at boot, and right now that knowledge exists only in the live system’s mount table, which is what genfstab -U reads. -U writes UUIDs rather than device paths, because /dev/sda is assigned in whatever order the kernel finds disks, while a UUID belongs to the filesystem and never moves.

setup writes the file rather than appending to it, so running it twice cannot duplicate an entry, and it stops if the swapfile is missing from the result.

3. Time zone, locale, keymap and hostname

/etc/localtime becomes a symlink into /usr/share/zoneinfo, and the hardware clock is set from the system clock. Your locale is uncommented in /etc/locale.gen and generated, and written to /etc/locale.conf as the default. The keymap goes into /etc/vconsole.conf and the hostname into /etc/hostname. There is no need for a 127.0.1.1 line in /etc/hosts: systemd’s myhostname resolver answers for the local hostname already.

Clocks on the Raspberry Pi

A Pi 5 has a real-time clock, but it only keeps time across a power cut if you have fitted the optional battery. Without one it boots with a meaningless date and waits for NTP to correct it, which is harmless as long as the network comes up. The same is true of the live system, and a wrong clock makes pacman’s signature checks and TLS fail in confusing ways – so if setup install fails on signatures on a Pi, check timedatectl first.

4. initramfs

To mount the root filesystem the kernel needs drivers – for the disk controller, for btrfs, maybe for unlocking LUKS – and those drivers live on the root filesystem it cannot mount yet. The initramfs is a second, tiny filesystem loaded into memory alongside the kernel with just enough in it to find the real root, mount it and hand over. Arch builds it with mkinitcpio.

The default HOOKS are already right for an unencrypted btrfs root, including the microcode hook that folds CPU microcode into the image (which is why the boot entry below has no separate intel-ucode.img line, unlike older guides). On an encrypted disk setup adds sd-encrypt after block. It has to be sd-encrypt, not plain encrypt: the default initramfs is systemd-based, and the older hook builds without complaint into an image that cannot unlock the disk.

mkinitcpio -P then rebuilds every image. Warnings about missing firmware for hardware you do not have are normal.

5. Bootloader

A machine can only start itself if three things line up: the firmware finds something to run, that something loads a kernel and the initramfs, and the kernel is told where its root filesystem is. The last part is where installs usually fail, and they fail the same way: the machine powers on, prints a little, and drops to an emergency shell.

On an x86_64 PC, setup runs bootctl install, which copies systemd-boot onto the ESP and registers it with the firmware, then writes its menu and one entry:

Edit this file: /boot/loader/loader.conf
default  arch.conf
timeout  3
console-mode keep
editor   no
Edit this file: /boot/loader/entries/arch.conf
title   Arch Linux
linux   /vmlinuz-linux
initrd  /initramfs-linux.img
options root=UUID=e1445380-51a2-449d-ad82-8ac3286cbfb6 rootflags=subvol=@ rw

editor no matters more than it looks: with the editor enabled, anyone at the keyboard can append init=/bin/sh to the kernel command line and get a root shell without a password.

On a Raspberry Pi 5 there is no bootloader to install. The Pi’s firmware reads config.txt and cmdline.txt from the FAT partition, loads the kernel and initramfs itself, and jumps straight in. config.txt already loads the initramfs, so setup leaves it alone and rewrites only the kernel command line, which ships pointing at an SD card:

Edit this file: /boot/cmdline.txt
root=UUID=e1445380-51a2-449d-ad82-8ac3286cbfb6 rootflags=subvol=@ rw rootwait console=serial0,115200 console=tty1 fsck.repair=yes

The firmware reads only the first line of that file, so if you edit it by hand, make sure your editor has not wrapped it. rootwait tells the kernel to wait for a USB or NVMe disk to appear rather than give up. Both files are pacman backup files of the kernel package, so a kernel update keeps your version and writes its own alongside as cmdline.txt.pacnew – which is how new firmware options arrive, and why .pacnew files are worth checking after kernel updates.

Either way, the part that is specific to this book’s layout is rootflags=subvol=@. Without it the kernel mounts the top of the btrfs filesystem, finds the subvolumes sitting there as directories, and fails to boot. The UUID is read from the filesystem setup disk made, never typed. On an encrypted disk, the root becomes rd.luks.name=<LUKS UUID>=cryptroot root=/dev/mapper/cryptroot – the UUID of the LUKS container, which is a different UUID from the btrfs filesystem inside it, and the easiest thing to get wrong when doing this by hand.

The Pi half is untested

The x86_64 half of this stage boots in BOOMSTICK’s test VM, encrypted or not: ENCRYPT=yes just test-vm installs onto LUKS, answers the initramfs passphrase prompt on the serial console, and logs in afterwards. The Raspberry Pi half – cmdline.txt, and a passphrase prompt with no bootloader behind it – has not yet booted a machine.

6. The host repo, applied

The files under the host repo’s etc/ are installed at the same paths under /mnt/etc, owned by root. git does not keep file modes, so setup sets them by place: 0440 for sudoers drop-ins, which sudo insists on before it will read them, and 0644 for everything else. Files under the repo’s bin/ – admin – go to /usr/local/bin, mode 0755. Then every unit in units.txt is enabled.

Enabling works inside the chroot because it only creates symlinks on disk. Nothing starts until the machine reboots: the systemd you can see from the chroot belongs to the live system, not the target.

The network this sets up by default is systemd-networkd for addresses, systemd-resolved for DNS and iwd for WiFi – the same combination the live system is running. The two .network files match by name prefix (en*, wl*) rather than exact name, so they work on any machine without checking what the kernel called the hardware, and a USB ethernet adapter plugged in later just works.

The sshd drop-in is three lines, and all three matter. PasswordAuthentication no alone is not enough: with PAM, keyboard-interactive is a second route to the same password prompt, so it is switched off explicitly, along with root login. Arch’s sshd_config includes /etc/ssh/sshd_config.d/*.conf, so the drop-in overrides the defaults without touching the shipped file, and package updates never conflict with it.

7. The live system’s side

Two things can only be done from outside the chroot, so setup does them from the live system against /mnt:

  • /etc/resolv.conf becomes a symlink to resolved’s stub. Inside the chroot that file is the live system’s own, bind-mounted over the target’s so that package downloads can resolve names, and replacing it there fails.
  • The live system’s iwd network files – the WiFi passphrases the stick joined with – are copied into /mnt/var/lib/iwd. This is the single most commonly forgotten step of a headless install: the machine boots, the disk is perfect, and it never joins the network because nothing told it the passphrase.

8. Accounts

admin and user are created, if they do not exist yet, with admin in wheel. visudo -c then checks that the whole sudo configuration parses. That check is not optional: a malformed sudoers file does not degrade gracefully – sudo refuses to run at all – and on a machine where sudo is the only route to root, that is a lockout you can only fix from the console.

admin’s ~/.ssh/authorized_keys is installed from the host repo, owned by admin, mode 0600, in a 0700 directory. Those modes are not decoration: sshd silently refuses an authorized_keys file that other users could write to, and from the client’s side you are just asked for a password that will never be accepted.

The daily user is given the same keys, so the first thing you do on the new machine can be logging in as yourself rather than as its administrator. The two are managed differently from then on: admin’s are installed from the repo on every run, because the repo is what says who may administer this machine, while the daily user’s are seeded once and left alone afterwards – a key added there by hand is theirs, and a re-run will not revert it.

9. Passwords

The only prompts after the review: a password for admin and one for the daily user, each typed twice. On a re-run, an account that already has one is skipped.

root is not among them. It is locked instead – no password at all, so there is none to guess, leak, or forget to rotate. Nothing needs it: admin is in wheel, administration happens through sudo, and sshd refuses root whatever its password is. setup check expects root=L.

What a locked root costs

systemd’s emergency and rescue targets ask for the root password, and sulogin will not open a shell for a locked account – it says so and reboots. A machine that cannot mount its root filesystem therefore cannot be rescued from its own keyboard.

This book makes that trade on purpose, because the stick is the recovery tool and a better one: boot it, then setup host --from and setup disk --mount as in Prepare the disk, and arch-chroot /mnt gives you a root shell inside the broken system – over SSH, which a keyboard attached to a machine in another room does not offer.

If you would rather have the console, sudo passwd root on the running machine sets one. setup check accepts a root password that exists; it objects only to an account with none.

10. aconfmgr

aconfmgr is what admin runs. It is in neither Arch’s nor Arch Linux ARM’s repositories – only the AUR has it, and building AUR packages would mean base-devel on every machine – but it is a bash program with no compiled parts, so setup clones it from GitHub at a fixed commit into /usr/local/lib/aconfmgr, and links it where its own launcher looks (/usr/lib/aconfmgr, /usr/local/bin/aconfmgr). Pinning the commit means every machine runs the version this book was checked against; a newer one is a deliberate change to setup.

11. The host repo moves in

setup writes aconfmgr/10-base.sh, the base packages it picked for this hardware, so aconfmgr knows them as wanted. A commit records the install in the host repo, which is then copied to /mnt/home/admin/arch and handed to admin. From the first boot, the description of the machine lives on the machine.

setup install finishes by running the check.

// settings
theme:
fx: