May 30, 2026·6 min read·engineeringnixbootsystemd

NixOS 26.05 makes early boot a systemd dependency graph

NixOS 26.05 switches stage 1 to systemd by default. Custom early-boot hooks now need explicit unit dependencies, and encrypted-root configurations have migration details that evaluation cannot always detect.

A custom early-boot command used to occupy a named position in the NixOS init script. With NixOS 26.05, that work belongs in a systemd unit with explicit dependencies. The Yarara release notes, dated May 30, make systemd the default stage 1 implementation and deprecate the older scripted path. For customized machines, the change concerns how boot ordering is represented.

The initrd has a difficult job before ordinary services can start. It must establish enough of the machine to reach the real root filesystem and hand control to the installed system. A command inserted before device setup is different from one that needs an unlocked disk. Moving those commands requires preserving their prerequisites, not simply placing their text in another option.

NixOS manages my personal infrastructure, and declarative state is the main reason I use it. Early boot is part of that state. A dependency graph is a useful representation when it says which resources an operation requires and when its result must be available. That is the part of this default change that interests me most.

The module generates a separate stage 1 environment

The systemd initrd module in 26.05 generates units specifically for the initrd. Its contents link /init to the systemd executable and provide a generated /etc/systemd/system. This is a separate early-boot environment built from declared contents and store paths.

That separation explains why a service configured for the ordinary running system is not automatically an early-boot service. The module exposes its own boot.initrd.systemd.services option, along with targets and other unit types. A command needed before the real root is available has to exist in the initrd environment and participate in its unit graph.

The source includes upstream initrd targets and switch-root services. NixOS adds activation-related units around that transition. This gives the early stage a defined handoff into the installed system. The final boot is still a sequence across environments, even though both stages now use related unit configuration mechanisms.

The option documentation says the initrd service options match their stage 2 counterparts, with exceptions for restart and reload triggers. Familiar unit syntax therefore helps, but stage-specific behavior remains explicit. A service intended to run before root activation has different dependencies from a daemon that starts after the machine has reached its ordinary filesystem layout.

Script hooks cannot be translated by name alone

The module asserts that several old command hooks must be empty under systemd stage 1. These include preDeviceCommands, postDeviceCommands, preLVMCommands, and postMountCommands. The error points users toward initrd systemd services and the boot ordering documentation. The assertion prevents a configuration from silently discarding an old hook.

A hook name supplied ordering through its position in a particular script. A unit supplies ordering through relationships with other units. The migration must identify what the command actually needs. Replacing a post-mount hook with a unit that has no mount dependency would preserve the command text while losing the condition that made its execution meaningful.

There is also a distinction between starting a unit and ordering it. A unit can declare when it should run relative to another operation, while still needing a relationship that causes it to enter the boot transaction. That distinction is visible in the module source, where units use relationships such as requiredBy alongside before. Reading only the script body misses this part of the generated behavior.

For example, the module orders its activation service before initrd-switch-root.service and makes that switch-root service require it. The exact arrangement is part of the NixOS implementation, rather than a generic recipe for every custom command. It illustrates why a migration needs both an activation relationship and an ordering relationship appropriate to its task.

The old utility-injection hooks also produce assertions. The replacements include initrdBin, extraBin, contents, and storePaths. These options describe different forms of inclusion. A unit can only execute a tool that exists in its environment, so moving a command without its required binaries is an incomplete translation.

Tool availability is part of the boot declaration

The initrd module builds a binary environment from declared package paths and links its binaries into the early filesystem. It separately collects store paths needed by systemd and the generated job scripts. This makes the image contents a construction concern alongside service ordering. The ordinary host search path does not define what exists before root activation.

The source distinguishes extraBin, which adds named tools under /bin, from contents, which links files into the image. Those choices are useful for different dependencies. A command might need an executable by a conventional name or a configuration file at an expected location. The declaration should reflect that requirement so the initrd can be built with the necessary material.

The module also has a shell option whose documentation says disabling it removes shells from the initrd. Paths that use shell scripts then break. That detail demonstrates the same dependency principle at a smaller scale. A service expressed through a script still needs an interpreter, even though systemd is responsible for scheduling it.

My preference is for these requirements to remain visible in the configuration that owns the early-boot operation. A hidden assumption about a utility in the running system is especially awkward here because the relevant filesystem may not yet be available. Declaring the dependency connects the operation to the image that must execute it.

Encrypted root has naming and prompting details

The release notes call out LUKS configurations that evaluation cannot fully diagnose. The root filesystem device should identify the mapper device corresponding to the named LUKS definition. A mismatch can leave systemd waiting for a device while a passphrase prompt is active. More complicated storage arrangements can need an explicit device-timeout option.

This is a concrete example of the difference between syntactically valid configuration and a bootable machine. The declarations can name individually plausible devices while failing to describe the relationship expected during unlock. A configuration evaluation cannot observe the actual sequence of device creation and user input on every storage layout.

The notes also replace the old cryptsetup-askpass procedure with systemctl default, which requests passphrases as needed. Remote prompting requires a terminal, and the guidance calls for forcing a TTY when using SSH in that situation. These are changes to the recovery interaction, not only the normal unattended path.

The boot configuration therefore includes an operator interface as well as a successful path. When an encrypted disk needs input, the machine must expose a usable prompt through the channel the operator has. Preserving an old remote-unlock command without accounting for the new prompting behavior can make that interface fail even though the storage keys remain correct.

The module also declares how root filesystem information reaches systemd. Its default root mode is fstab, and an assertion requires a root entry in fileSystems for that mode. The generated kernel parameters identify the chosen interpretation. That ties an ordinary NixOS filesystem declaration to early boot without requiring every user to handwrite a mount unit. Custom storage work still has to preserve the device relationships beneath that declaration, since the assertion checks the presence of a root configuration rather than proving that the hardware will supply its device.

The fallback has a limited maintenance role

The release permits a temporary return to the scripted implementation. The option is explicit, and the notes discourage keeping it as the long-term response to migration problems.

{
  boot.initrd.systemd.enable = false;
}

This restores the older stage 1 selection for the transition period. It does not translate a custom hook into the systemd model. The release notes schedule removal of the old implementation for 26.11, which makes the fallback a bounded migration tool as of this release.

A successful configuration build also cannot demonstrate the entire early-boot interaction. The interesting behavior occurs when the kernel starts the image, devices appear, and the root filesystem becomes usable. That is why the release separates evaluation assertions from issues that only become visible during boot. Both categories follow directly from where the configuration takes effect.

For a standard configuration, the new default moves early boot onto the supported systemd route without requiring a custom service declaration. For a machine with additional commands, the migration exposes previously positional assumptions as dependencies. The resulting declaration should say which resources the command needs and how its completion relates to root activation, with its tools included in the same initrd construction.