Systemd Stage 1 Initrd
Systemd Stage 1 Initrd
NixOS 26.05 made systemd Stage 1 the default initrd. The old busybox/scripted Stage 1 is deprecated and scheduled for removal in 26.11. The boring default is: leave boot.initrd.systemd.enable on (the 26.05 default), point root at /dev/mapper/<name> for LUKS, express early boot work as boot.initrd.systemd.services / .mounts, and debug with rd.systemd.debug_shell — do not flip the switch back to scripted Stage 1 to “make it work.”
Stage 2 systemd (the services chapter) already runs after pivot-root. Stage 1 is the same language, earlier: unlock disks, mount /sysroot, run oneshots, then hand off.
Mental model
firmware / UKI / systemd-boot
│
▼
┌───────────────────────────────┐
│ Stage 1 (initrd) │
│ systemd PID 1 in the initrd │
│ cryptsetup · mounts · fsck │
│ custom oneshots │
│ sysroot.mount → switch-root │
└───────────────┬───────────────┘
▼
┌───────────────────────────────┐
│ Stage 2 (real root) │
│ systemd.services.* │
│ multi-user.target │
└───────────────────────────────┘
| Concern | Scripted Stage 1 (legacy) | Systemd Stage 1 (26.05 default) |
|---|---|---|
| Orchestration | Shell snippets, preLVMCommands, order by string concat |
Units, generators, RequiresMountsFor= |
| LUKS | cryptsetup in a script |
systemd-cryptsetup@ from boot.initrd.luks.devices |
| Key file on a filesystem | Mount by hand in a script | boot.initrd.systemd.mounts + automatic deps |
| FIDO2 / TPM2 unlock | Awkward / third-party | crypttabExtraOpts + systemd-cryptenroll |
| Emergency shell | Busybox ash tricks | systemctl, journalctl, rd.systemd.debug_shell |
| Root in initrd | /mnt-root |
/sysroot |
On 26.05 you rarely set boot.initrd.systemd.enable = true; — it is already true. Explicit = true in snippets below is documentation, not a requirement for new hosts. = false is a 26.11 landmine. Debug with rd.systemd.debug_shell on the kernel cmdline (Stage 1 tty), not by reverting to scripted initrd.
LUKS mapped names in Stage 1 are /dev/mapper/<name> from boot.initrd.luks.devices.<name>. Disko’s LUKS name must match that <name> or sysroot.mount waits forever. Root is /sysroot in the initrd, / after switch-root — a custom oneshot that writes /etc in Stage 1 is writing the initrd overlay, not persist.
Worked examples
Case 1: Confirm Stage 1 is systemd on a desk workstation
Save as initrd_probe.nix and import it from the host flake:
# initrd_probe.nix
{ lib, ... }:
{
# Explicit for clarity on a 26.05 desk host. Omit on greenfield configs.
boot.initrd.systemd.enable = true;
# Keep a few generations so a bad initrd is recoverable from the boot menu.
boot.loader.systemd-boot.configurationLimit = 8;
}After nixos-rebuild switch (and a reboot if you just upgraded from 25.11):
# Is PID 1 in the initrd systemd? Check the running system metadata:
systemctl --version | head -n1
lsinitrd /run/current-system/initrd 2>/dev/null | rg -i 'systemd|initrd' | head
cat /run/current-system/kernel-paramsRepresentative output on a healthy 26.05 desk host:
systemd 257 (257.x)
...
drwxr-xr-x 1 root root 0 Jan 1 00:00 etc/systemd
...
init=/nix/store/…-systemd-stage-1-init/init
If you still see boot.initrd.systemd.enable = false somewhere in the merge, find it:
nixos-option boot.initrd.systemd.enableValue:
true
Default:
true
Case 2: LUKS root that does not time out
The #1 upgrade breakage: fileSystems."/".device still points at the raw disk (or a by-uuid of the encrypted partition) while systemd Stage 1 waits for a device unit that never becomes the mounted root. Point / at the mapper name that matches boot.initrd.luks.devices.<name>.
Save as luks_root.nix:
# luks_root.nix
{ lib, ... }:
{
boot.initrd.systemd.enable = true;
boot.initrd.luks.devices.cryptroot = {
device = "/dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d";
# Optional: allow FIDO2 tokens enrolled with systemd-cryptenroll.
# crypttabExtraOpts = [ "fido2-device=auto" ];
};
fileSystems."/" = {
device = "/dev/mapper/cryptroot";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/ABCD-1234";
fsType = "vfat";
options = [ "umask=0077" ];
};
# Complex stacks (LVM on LUKS, slow USB keyfiles): never time out the root device.
# Prefer fixing the mapper path first; use infinity only when the topology needs it.
# fileSystems."/".options = [ "x-systemd.device-timeout=infinity" ];
}Build without activating (safe on a laptop that is not this host):
nixos-rebuild build --flake .#desk-workstationbuilding the system configuration...
/nix/store/xxxxxxxx-nixos-system-desk-workstation-26.05
Emergency kernel cmdline if you already cannot boot (from the bootloader editor):
systemd.default_device_timeout_sec=infinity
Then fix fileSystems."/".device and rebuild.
Case 3: Keyfile on a separate filesystem — mounts, not sleep scripts
Scripted Stage 1 taught people to sleep 2; mount … in boot.initrd.systemd.services. With systemd Stage 1, declare the mount. systemd-cryptsetup grows a RequiresMountsFor= on the key path automatically.
Save as luks_keyfile_mount.nix:
# luks_keyfile_mount.nix
{
boot.initrd.systemd.enable = true;
boot.initrd.systemd.mounts = [
{
what = "UUID=b501f1b9-7714-472c-988f-3c997f146a17";
where = "/key";
type = "ext4";
options = "ro";
}
];
boot.initrd.luks.devices.cryptroot = {
device = "/dev/disk/by-uuid/3f6b0024-3a44-4fde-a43a-767b872abe5d";
keyFile = "/key/root.key";
};
fileSystems."/" = {
device = "/dev/mapper/cryptroot";
fsType = "btrfs";
options = [ "subvol=@" ];
};
}Dependency chain systemd builds for you:
UUID=… device appears
→ key.mount
→ systemd-cryptsetup@cryptroot.service (RequiresMountsFor=/key/root.key)
→ sysroot.mount (/dev/mapper/cryptroot)
→ initrd.target → switch-root
Do not put /mnt-root in a key path. Under systemd Stage 1 the real-root mount is /sysroot.
Case 4: A Stage 1 oneshot for the desk (impermanence-friendly)
Ephemeral-root desks often need a tiny action before sysroot is writable enough for Stage 2 — for example, ensuring a Btrfs subvolume exists, or wiping / except persisted paths. Express it as an initrd service with DefaultDependencies = "no" and explicit ordering.
Save as initrd_wipe_root.nix:
# initrd_wipe_root.nix
{ pkgs, ... }:
{
boot.initrd.systemd.enable = true;
boot.initrd.systemd.services.desk-prepare-root = {
description = "Desk: prepare ephemeral root before switch-root";
wantedBy = [ "initrd.target" ];
before = [ "sysroot.mount" ];
unitConfig.DefaultDependencies = "no";
serviceConfig.Type = "oneshot";
# Store paths only — no ambient PATH in Stage 1.
path = [ pkgs.coreutils pkgs.btrfs-progs ];
script = ''
set -eu
# Illustrative: create a subvolume the Stage 2 impermanence module expects.
# Real desks wire this to the same layout as the Disko / impermanence chapters.
if ! btrfs subvolume show /sysroot/@ 2>/dev/null; then
echo "desk-prepare-root: @ missing — check Disko layout"
fi
'';
};
}Inspect whether the unit is in the system closure after a dry build:
nixos-rebuild build --flake .#desk-workstation
nix-store -q --references result | rg -i 'initrd|systemd' | headWhile debugging a live Stage 1 (see Case 5), systemctl status desk-prepare-root.service shows the oneshot before switch-root. After switch-root those units are gone — that is expected.
Case 5: Debug a stuck Stage 1 with the emergency shell
When sysroot.mount fails, you need a shell inside the initrd, not Stage 2 rescue.target.
- At the systemd-boot (or GRUB) editor, append:
rd.systemd.debug_shell
Boot. A root shell on tty9 (often Alt+F9) appears while Stage 1 is running.
Useful commands in that shell:
systemctl
systemctl status sysroot.mount
systemctl status systemd-cryptsetup@cryptroot.service
journalctl -b -u sysroot.mount
systemctl default # retry reaching initrd.target; also runs ask-password agentSave a flake fragment that keeps the debug shell available without typing cmdline every time on a lab VM only:
# initrd_debug_lab.nix
{
boot.initrd.systemd.enable = true;
# Lab VMs only — this is a root shell before the root filesystem is trusted.
boot.kernelParams = [ "rd.systemd.debug_shell" ];
}Never leave rd.systemd.debug_shell on a production desk host that sits in an unlocked room.
The trap
Trap A — flip boot.initrd.systemd.enable = false to silence a LUKS timeout. That restores the deprecated scripted initrd. It will be removed in 26.11. You then maintain two boot stories and break FIDO2/systemd-cryptenroll paths. Fix the mapper device and mount units instead.
Trap B — keep postDeviceCommands / preLVMCommands / /mnt-root assumptions. Those are scripted-Stage-1 APIs. Under systemd Stage 1, early work is units; the root mount is /sysroot. Copied blog snippets from 23.11 often fail silently or race.
Trap C — keyfile = /dev/mapper/usb as a raw device. Newer systemd crypttab behaviour expects a filesystem+path form (/key:/dev/mapper/usb) or a real mount unit + file path. Raw-device keyfiles that “worked” under patched older NixOS can stall after upgrade.
# Anti-pattern — do not "fix" Stage 1 by reverting
{
boot.initrd.systemd.enable = false; # deprecated; dies in 26.11
boot.initrd.postDeviceCommands = ''
sleep 2
…
'';
}# Boring fix — stay on systemd Stage 1
{
boot.initrd.luks.devices.cryptroot.device = "/dev/disk/by-uuid/…";
fileSystems."/".device = "/dev/mapper/cryptroot";
boot.initrd.systemd.mounts = [ { what = "UUID=…"; where = "/key"; type = "ext4"; } ];
}The boring rule
- Keep systemd Stage 1 enabled on NixOS 26.05+. Treat
enable = falseas a temporary incident control, not a config style. - LUKS root:
boot.initrd.luks.devices.<name>andfileSystems."/".device = "/dev/mapper/<name>". - Key material on another filesystem:
boot.initrd.systemd.mounts, notsleep+mountoneshots. - Custom early boot:
boot.initrd.systemd.serviceswithDefaultDependencies = "no"andbefore = [ "sysroot.mount" ];. - Paths:
/sysroot, never/mnt-root, in Stage 1 scripts. - Debug with
rd.systemd.debug_shellandsystemctl status; fix units; then remove the debug cmdline. - Disko still owns partitioning; this chapter owns how Stage 1 opens what Disko created.
Try this
- On a disposable 26.05 VM, run
nixos-option boot.initrd.systemd.enableand confirmtrue. Forcefalse, rebuild, note the eval/boot warnings, then set it back. - Take the Disko LUKS layout from the Disko chapter. Verify
fileSystems."/".deviceis/dev/mapper/…matching the LUKS name. Mis-point it at the raw by-uuid,nixos-rebuild build-vm, and watch Stage 1 complain in the serial log. - Add a
boot.initrd.systemd.mountsentry for a second disk labeledKEYSand unlock withkeyFile = "/key/root.key". Confirm order with unit status from a failedrd.systemd.debug_shellsession. - Enroll a FIDO2 token with
systemd-cryptenroll --fido2-device=auto /dev/disk/by-uuid/…, setcrypttabExtraOpts = [ "fido2-device=auto" ];, and boot once with the token present and once without (expect passphrase fallback if a passphrase slot remains). - Schedule the removal: grep the desk flake for
postDeviceCommands,preLVMCommands, andboot.initrd.systemd.enable = false. Open tickets for each hit before 26.11.