Import an upstream NixOS module¶
System Manager ships with a curated set of NixOS modules (nginx, ACME, userborn, nix settings), but the NixOS module library contains hundreds more.
You can import additional NixOS modules into your system-manager configuration by referencing them through the nixosModulesPath special argument and providing stubs for any options the module expects but that system-manager does not define.
This guide walks through the technique using real patterns from the system-manager codebase.
How NixOS modules are loaded¶
When you call makeSystemConfig, system-manager passes nixosModulesPath as a special argument available to all modules.
This path points to the NixOS modules directory inside nixpkgs (<nixpkgs>/nixos/modules), giving you access to the entire NixOS module library.
An upstream NixOS module is imported by mapping its path relative to that directory:
In practice, most NixOS modules reference options defined by other NixOS modules that system-manager does not include. When the module system encounters an undefined option, evaluation fails with an error like:
The solution is to declare stub options that satisfy the module's references without implementing the underlying functionality.
Creating stub options¶
A stub option declares an option name with a type and default value so that module evaluation succeeds.
The simplest form uses lib.types.raw, which accepts any value without validation:
This is the pattern system-manager uses internally for boot.*, which many NixOS modules reference but which has no meaning outside NixOS.
When you need the stub to carry a specific default value (for example, an empty list or false), use a concrete type:
This is the actual stub that system-manager uses to allow sops-nix to evaluate.
The sops-nix module reads services.openssh.hostKeys to auto-detect SSH keys for age decryption; the empty default means no keys are auto-detected, and users set sops.age.sshKeyPaths explicitly instead.
Automatic target substitution¶
NixOS services typically declare wantedBy = [ "multi-user.target" ].
System-manager automatically rewrites multi-user.target to system-manager.target in all wantedBy and requiredBy declarations during unit generation, so imported NixOS modules work without manual target adjustment.
This means you can use wantedBy = [ "multi-user.target" ] in your service definitions exactly as you would in NixOS, and system-manager will do the right thing.
Step-by-step walkthrough¶
This walkthrough imports the saslauthd module (Cyrus SASL authentication daemon) from nixpkgs. It is a self-contained 58-line module that defines a single systemd service with no external NixOS module dependencies, making it a straightforward candidate.
1. Try to build¶
Create a module that imports saslauthd:
The upstream module declares wantedBy = [ "multi-user.target" ], which system-manager automatically substitutes with system-manager.target.
Add it to your makeSystemConfig modules list:
Then build:
Because this module only uses systemd.services, which system-manager already provides, no stubs are needed and the build succeeds.
2. When stubs are needed¶
If the build had failed with an error like:
you would add stub options to the same module (or a separate file) to satisfy the reference, as described in creating stub options above.
3. Configure the service¶
The upstream module options are now available in your configuration:
4. Verify¶
Build the configuration to confirm it evaluates without errors:
Real-world example: sops-nix¶
The sops-nix module is the codebase's reference for a module that needed more than plain stubs.
Upstream sops-nix installs secrets in one of two ways: from a system.activationScripts.setupSecrets snippet, or from a sops-install-secrets.service oneshot unit when sops.useSystemdActivation is set.
System-manager never runs NixOS activation scripts, so only the second path can work.
nix/modules/upstream/sops-nix.nix therefore does three things:
- it declares the
system.activationScripts.{setupSecrets,setupSecretsForUsers,generate-age-key}options, so that sops-nix evaluates even on the code paths system-manager does not take; - it defaults
sops.useSystemdActivationtotrueand asserts that it stays enabled, so that secrets management cannot be silently disabled; - it re-points the units sops-nix orders around NixOS-only services (
systemd-sysusers.service) at their system-manager equivalents, and lifts thegenerate-age-keyactivation script into asops-generate-age-key.serviceoneshot.
The pattern generalises: declare stub options to make a module evaluate, then translate whatever the module expresses as activation scripts into systemd units ordered against sysinit-reactivation.target, which system-manager restarts on every activation.
See Manage secrets with sops-nix for the user-facing guide.
Tips¶
Not every NixOS module can work with system-manager.
Modules that depend on kernel features (boot.*), the NixOS activation system, or NixOS-specific infrastructure like the NixOS module evaluator may require more extensive stubs or may not be practical to port.
Services that only need systemd units, /etc files, and packages are the best candidates.
When evaluating whether a module is worth importing, read its source in nixpkgs to understand its dependencies. A module that primarily generates a systemd service and a configuration file is straightforward to adapt. A module that deeply integrates with the NixOS activation system or requires multiple other NixOS modules will need proportionally more stubs.
Troubleshooting¶
Option type conflict with built-in modules¶
If you previously created a stub for an option that system-manager now provides as a built-in module, evaluation will fail with a type conflict error:
This happens when a catch-all stub like
conflicts with system-manager's structured option declarations for the same path.
The Nix module system cannot reconcile a flat lazyAttrsOf raw type with nested typed options defined by another module.
The fix is to remove your stub.
System-manager's built-in module already defines the options with proper types and defaults, so the stub is no longer needed.
If your imported NixOS module sets firewall options like networking.firewall.allowedTCPPorts, system-manager will accept them and emit a warning listing the configured ports, since it does not manage firewall rules on the host.
This pattern applies to any option path where your configuration declares a broad type (raw, lazyAttrsOf raw) and a system-manager module later introduces structured options under the same path.
When you see this class of error, check whether system-manager now ships a module covering those options and remove the redundant stub.
See also¶
- Module options reference for options already available in system-manager
- NixOS comparison for differences between NixOS and system-manager
- Test configuration for validating your configuration with container tests