Systemd Stage 1 Initrd

Updated

September 12, 2026

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-params

Representative 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.enable
Value:
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-workstation
building 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' | head

While 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.

  1. At the systemd-boot (or GRUB) editor, append:
rd.systemd.debug_shell
  1. Boot. A root shell on tty9 (often Alt+F9) appears while Stage 1 is running.

  2. 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 agent

Save 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 = false as a temporary incident control, not a config style.
  • LUKS root: boot.initrd.luks.devices.<name> and fileSystems."/".device = "/dev/mapper/<name>".
  • Key material on another filesystem: boot.initrd.systemd.mounts, not sleep + mount oneshots.
  • Custom early boot: boot.initrd.systemd.services with DefaultDependencies = "no" and before = [ "sysroot.mount" ];.
  • Paths: /sysroot, never /mnt-root, in Stage 1 scripts.
  • Debug with rd.systemd.debug_shell and systemctl status; fix units; then remove the debug cmdline.
  • Disko still owns partitioning; this chapter owns how Stage 1 opens what Disko created.

Try this

  1. On a disposable 26.05 VM, run nixos-option boot.initrd.systemd.enable and confirm true. Force false, rebuild, note the eval/boot warnings, then set it back.
  2. Take the Disko LUKS layout from the Disko chapter. Verify fileSystems."/".device is /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.
  3. Add a boot.initrd.systemd.mounts entry for a second disk labeled KEYS and unlock with keyFile = "/key/root.key". Confirm order with unit status from a failed rd.systemd.debug_shell session.
  4. Enroll a FIDO2 token with systemd-cryptenroll --fido2-device=auto /dev/disk/by-uuid/…, set crypttabExtraOpts = [ "fido2-device=auto" ];, and boot once with the token present and once without (expect passphrase fallback if a passphrase slot remains).
  5. Schedule the removal: grep the desk flake for postDeviceCommands, preLVMCommands, and boot.initrd.systemd.enable = false. Open tickets for each hit before 26.11.