NixOS Modules and Option Merging
- Type: Tutorial
- Status: Current
- Scope: Reading imports, option declarations, enable gates, and merge priorities in
nixos-config- Canonical sources:
modules/common/base.nix;modules/common/default-config.nix;modules/services/jellyfin.nix;modules/services/service-failure-monitor.nix;hosts/lyra/default.nix;hosts/electra/default.nix; Nix module system tutorial- Last verified: Source commit
556df88494686003b1c4f20c8e0b99b5afc16a6e(2026-09-12); source HEADb2831d33132967b4c4fb69e6068b2b05fff8fdffchanges onlyflake.lock. Cited examples checked 2026-09-13; no module changed.- Review triggers: Module imports/options, service enable gates, specialisation overrides, or upstream module-system explanation
Learning goal and prerequisites
Understand how several files contribute to one host configuration without being executed as a shell script in order. Start after Nix Language Essentials and Flakes and Inputs. Use the official module-system tutorial for general mechanics.
Import graph and option values
flake.nix imports hosts/lyra/default.nix through makeNixosSystem.
Lyra's imports includes common/server modules, hardware, and selected
services. modules/common/base.nix imports default options, SOPS identity,
maintenance, and the service-failure monitor. Imports determine which
definitions participate; they are not activation commands or an override
sequence. Repository Structure and Import
Chain maps the full path.
modules/common/default-config.nix declares nixosConfig.host.role as an
enum whose default is "unspecified". Lyra sets the value to "server".
The module system combines the option declaration and definitions into one
typed result. A misspelled or undeclared option produces an error; two
incompatible definitions of a single-valued option conflict rather than
letting the later file win.
modules/services/jellyfin.nix declares an enable option and places its
service definitions under lib.mkIf cfg.enable. Importing the module makes
the option known; Lyra separately sets nixosConfig.jellyfin.enable = true.
By contrast, modules/hardware/bluetooth.nix sets Bluetooth options when
imported, without a local enable gate. Adding a Module or
Profile explains when each pattern fits.
Merging and priority in this fleet
Jellyfin contributes its data directory to nixosConfig.backup.dataPaths
and its unit name to services.serviceFailureMonitor.services using
lib.mkAfter. These list contributions merge with those of other services;
mkAfter controls ordering, not whether the unit runs. The monitor module
declares its service-name option as types.listOf types.str with an empty
default and uses the merged names to generate failure hooks.
lib.mkDefault makes a low-priority definition: Lyra's generated hardware
configuration uses it for nixpkgs.hostPlatform. lib.mkForce is a strong
override: Electra uses it inside igpu and dgpu specialisations for
values intentionally different from the base, including their labels.
Neither modifier is ordinary Nix assignment order. Use mkForce for an
explained override, not to hide an accidental conflict. List and attrset
options have their own merge rules, too; ordinary Nix // is not the same
thing as a NixOS module merge.
The final config is a fixed point. Definitions can refer to other merged
values, as Jellyfin's module does with config.services.jellyfin.dataDir.
It is not a mutable object filled in line by line. Circular dependencies
can produce infinite recursion; trace the option dependency rather than
blindly adding a priority modifier.
Guided check
In a read-only checkout, trace nixosConfig.jellyfin.enable from its
declaration to Lyra's value and the guarded service configuration. Find
Jellyfin's two mkAfter contributions and the declaration of
services.serviceFailureMonitor.services. Explain why importing the
monitor module alone does not monitor every service.
Compare the mkDefault host-platform value in
hosts/lyra/hardware-configuration.nix with an Electra specialisation
mkForce value. Do not change hardware mode as an exercise. For actual
edits, follow Adding a Module or
Profile and Testing and CI
Contracts.
Common mistakes: assuming an import enables an option-gated service,
assuming a later file wins a conflict, using mkForce to silence a design
error, or treating a backup-list entry as evidence of a usable snapshot.