| .. | ||
| portable | ||
| systemd | ||
| README.md | ||
Modular Services
This directory defines a modular service infrastructure for NixOS. See the Modular Services chapter in the manual [source].
Design decision log
Initial design
-
system.services.<name>. Alternatives consideredsystemServices: similar to does not allow importing a composition of services intosystem. Not sure if that's a good idea in the first place, but I've kept the possibility open.services.abstract: used in https://github.com/NixOS/nixpkgs/pull/267111, but too weird. Service modules should fit naturally into the configuration system. Also "abstract" is wrong, because it has submodules - in other words, evalModules results, concrete services - not abstract at all.services.modular: only slightly better thanservices.abstract, but still weird
-
No
daemon.*options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521 -
For now, do not add an
enableoption, because it's ambiguous. Does it disable at the Nix level (not generate anything) or at the systemd level (generate a service that is disabled)? -
Move all process options into a
processoption tree. Putting this at the root is messy, because we also have sub-services at that level. Those are rather distinct. Grouping them "by kind" should raise fewer questions. -
modules/system/service/systemd/system.nixhassystemtwice. Not great, but- they have different meanings
- These are system-provided modules, provided by the configuration manager
systemd/systemconfigures SystemD system units.
- This reserves
modules/servicefor actual service modules, at least until those are lifted out of NixOS, potentially
- they have different meanings
Configuration Data (configData) Design
Without a mechanism for adding files, all configuration had to go through process.*, requiring process restarts even when those would have been avoidable.
Many services implement automatic reloading or reloading on e.g. SIGUSR1, but those mechanisms need files to read. configData provides such files.
Naming and Terminology
-
configDatainstead ofenvironment.etc: The nameconfigDatais service manager agnostic. While systemd system services can use/etc, other service managers may expose configuration data differently (e.g., different directory, relative paths). -
pathattribute: EachconfigDataentry automatically gets apathattribute set by the service manager implementation, allowing services to reference the location of their configuration files. These paths themselves are not subject to change from generation to generation; only their contents are. -
nameattribute: Inenvironment.etcthis would betargetbut that's confusing, especially for symlinks, as it's not the symlink's target.
Service Manager Integration
-
Portable base: The
configDatainterface is declared inportable/config-data.nix, making it available to all service manager implementations. -
Systemd integration: The systemd implementation (
systemd/system.nix) mapsconfigDataentries toenvironment.etcentries under/etc/system-services/. -
Path computation:
systemd/config-data-path.nixrecursively computes unique paths for services and sub-services (e.g.,/etc/system-services/webserver/vs/etc/system-services/webserver-api/). Fun fact: for the module system it is a completely normal module, despite its recursive definition. If we parameterize/etc/system-services, it will have to become animportApplystyle module nonetheless (function returning module). -
Simple attribute structure: Unlike
environment.etc,configDatauses a simpler structure with justenable,name,text,source, andpathattributes. Complex ownership options were omitted for simplicity and portability. Per-service user creation is still TBD.
No pkgs module argument
The modular service infrastructure avoids exposing pkgs as a module argument to service modules. Instead, derivations and builder functions are provided through lexical closure, making dependency relationships explicit and avoiding uncertainty about where dependencies come from.
Benefits
- Explicit dependencies: Services declare what they need rather than implicitly depending on
pkgs - No interference: Service modules can be reused in different contexts without assuming a specific
pkgsinstance. An unexpectedpkgsversion is not a failure mode anymore. - Clarity: With fewer ways to do things, there's no ambiguity about where dependencies come from (from the module, not the OS or service manager)
Implementation
-
Portable layer: Service modules in
portable/do not receivepkgsas a module argument. Any required derivations must be provided by the caller. -
Systemd integration: The
systemd/system.nixmodule importsconfig-data.nixas a function, providingpkgsin lexical closure:(import ../portable/config-data.nix { inherit pkgs; }) -
Service modules:
- Should explicitly declare their package dependencies as options rather than using
pkgsdefaults:
{ # Bad: uses pkgs module argument foo.package = mkOption { default = pkgs.python3; # ... }; }{ # Good: caller provides the package foo.package = mkOption { type = types.package; description = "Python package to use"; defaultText = lib.literalMD "The package that provided this module."; }; }passthru.servicescan still provide a complete module using the package's lexical scope, making the module truly self-contained:
Package (
package.nix):{ lib, writeScript, runtimeShell, # ... other dependencies }: stdenv.mkDerivation (finalAttrs: { # ... package definition passthru.services.default = { imports = [ (lib.modules.importApply ./service.nix { inherit writeScript runtimeShell; }) ]; someService.package = finalAttrs.finalPackage; }; })Service module (
service.nix):# Non-module dependencies (importApply) { writeScript, runtimeShell }: # Service module { lib, config, options, ... }: { # Service definition using writeScript, runtimeShell from lexical scope process.argv = [ (writeScript "wrapper" '' #!${runtimeShell} # ... wrapper logic '') # ... other args ]; } - Should explicitly declare their package dependencies as options rather than using