Skip to content

Modular services #372170

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions nixos/doc/manual/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ let
inherit (pkgs) buildPackages runCommand docbook_xsl_ns;

inherit (pkgs.lib)
evalModules
hasPrefix
removePrefix
flip
Expand Down Expand Up @@ -116,8 +117,35 @@ let
${testOptionsDoc.optionsJSON}/${common.outputPath}/options.json
sed -e '/@PYTHON_MACHINE_METHODS@/ {' -e 'r ${testDriverMachineDocstrings}/machine-methods.md' -e 'd' -e '}' \
-i ./development/writing-nixos-tests.section.md
substituteInPlace ./development/modular-services.md \
--replace-fail \
'@PORTABLE_SERVICE_OPTIONS@' \
${portableServiceOptions.optionsJSON}/${common.outputPath}/options.json
substituteInPlace ./development/modular-services.md \
--replace-fail \
'@SYSTEMD_SERVICE_OPTIONS@' \
${systemdServiceOptions.optionsJSON}/${common.outputPath}/options.json
'';

portableServiceOptions = buildPackages.nixosOptionsDoc {
inherit (evalModules { modules = [ ../../modules/system/service/portable/service.nix ]; }) options;
inherit revision warningsAreErrors;
transformOptions = opt: opt // {
# Clean up declaration sites to not refer to the NixOS source tree.
declarations = map stripAnyPrefixes opt.declarations;
};
};

systemdServiceOptions = buildPackages.nixosOptionsDoc {
inherit (evalModules { modules = [ ../../modules/system/service/systemd/service.nix ]; }) options;
# TODO: filter out options that are not systemd-specific, maybe also change option prefix to just `service-opt-`?
inherit revision warningsAreErrors;
transformOptions = opt: opt // {
# Clean up declaration sites to not refer to the NixOS source tree.
declarations = map stripAnyPrefixes opt.declarations;
};
};

in
rec {
inherit (optionsDoc) optionsJSON optionsNix optionsDocBook;
Expand Down
1 change: 1 addition & 0 deletions nixos/doc/manual/development/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ writing-documentation.chapter.md
nixos-tests.chapter.md
developing-the-test-driver.chapter.md
testing-installer.chapter.md
modular-services.md
```
98 changes: 98 additions & 0 deletions nixos/doc/manual/development/modular-services.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@

# Modular Services {#modular-services}

Status: in development. This functionality is new in NixOS 25.05, and significant changes should be expected. We'd love to hear your feedback in <https://github.com/NixOS/nixpkgs/pull/372170>

Traditionally, NixOS services were defined using sets of options *in* modules, not *as* modules. This made them non-modular, resulting in problems with composability, reuse, and portability.

A configuration management framework is an application of `evalModules` with the `class` and `specialArgs` input attribute set to particular values.
NixOS is such a configuration management framework, and so are [Home Manager](https://github.com/nix-community/home-manager) and [`nix-darwin`](https://github.com/lnl7/nix-darwin).

The service management component of a configuration management framework is the set of module options that connects Nix expressions with the underlying service (or process) manager.
For NixOS this is the module wrapping [`systemd`](https://systemd.io/), on `nix-darwin` this is the module wrapping [`launchd`](https://en.wikipedia.org/wiki/Launchd).

A *modular service* is a [module] that defines values for a core set of options declared in the service management component of a configuration management framework, including which program to run.
Since it's a module, it can be composed with other modules via `imports` to extend its functionality.

NixOS provides two options into which such modules can be plugged:

- `system.services.<name>`
- an option for user services (TBD)

Crucially, these options have the type [`attrsOf`] [`submodule`].
The name of the service is the attribute name corresponding to `attrsOf`.
<!-- ^ This is how composition is *always* provided, instead of a difficult thing (but this is reference docs, not a changelog) -->
The `submodule` is pre-loaded with two modules:
- a generic module that is intended to be portable
- a module with systemd-specific options, whose values or defaults derive from the generic module's option values.

So note that the default value of `system.services.<name>` is not a complete service. It requires that the user provide a value, and this is typically done by importing a module. For example:

<!-- Not using typical example syntax, because reading this is *not* optional, and should it should not be folded closed. -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That examples are closed by default is a skin issue, not a semantic issue. No one forces us to fold examples into <details> tags.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but I have to work with the tools I'm given.
Did you mean to provide a suggestion or is this fyi?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this?: an example is meant to be fully formed thing, whereas this here is just fancy pseudocode and not a true example because it is not a complete application of the subject discussed.

```nix
{
system.services.my-service-instance = {
imports = [ pkgs.some-application.services.some-service-module ];
foo.settings = {
# ...
};
};
}
```

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Configuration files {#modular-service-configuration-files}
If the service needs to create a configuration file (or multiple configuration files) for the program being run, it should follow NixOS/RFCs#42 in the following way.
* The generated configuration files should be made available under the attribute paths of the form `settingsFiles.<filename>`
* If there is a single configuration file, or there is a clear main configuration file, the options responsible for its entries should be placed under attribute paths of the form `settings.<setting>`, or the file should be treated the same as auxiliary configuration files.
* The options responsible for the entries of auxiliary files (and the main configuration file, if the module maintainer chooses so) should be places under attribute paths of the form `settingsGroups.<filename>.<setting>`
* The module should define the list of the configuration files it expects to generate (if any); open-ended `attrsOf` for `settingsFiles` or `settingsGroups` should not be used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tell me if this should be a PR-towards-PR-branch instead, it felt short enough to discuss as a suggestion.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I discussed this with @roberth in person a bit more in-depth, here's the take-away:

There should be a documented convention that generated files should be put in their own option, for the purpose of being able to override it, but:

  • This would not be a modular-services specific convention, but rather a general module system convention, and so we should put that in this manual section, and refer to it in the modular service docs.
  • The convention does not need to go beyond "a settingsFile option should be defined as the file generated from settings". It's unclear what would be the concrete benefits of standardizing on settingsFiles or settingsGroups.
  • Such a convention is not urgent and orthogonal to this PR, it could've been written before this PR and it can be written after it. So we shouldn't block on this.

## Portability {#modular-service-portability}

It is possible to write service modules that are portable. This is done by either avoiding the `systemd` option tree, or by defining process-manager-specific definitions in an optional way:

```nix
Copy link
Contributor

@fricklerhandwerk fricklerhandwerk Mar 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs some verbal annotation that this is a service module, because given the introduction so far it's not easy to figure out just from the _class. Which is why I'd argue we should use actual examples, since they support free-form annotations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

free-form annotations

Haven't heard of that. Can't find anything about examples, markdown or commonmark in https://github.com/NixOS/nixpkgs/blob/master/CONTRIBUTING.md

Copy link
Contributor

@fricklerhandwerk fricklerhandwerk Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/NixOS/nixpkgs/blob/master/doc/README.md#admonitions

By annotation I mean simply freeform text contained in the example block. And again, that examples are rendered collapsed by default is simply a misfeature and I've seen it in different places on the web recently, no clue why people would by default ignore the most important parts of documentation.

{ config, options, lib, ... }: {
_class = "service";
config = {
process.executable = "${lib.getExe config.foo.program}";
} // lib.optionalAttrs (options?systemd) {
# ... systemd-specific definitions ...
};
}
```

This way, the module can be loaded into a configuration manager that does not use systemd, and the `systemd` definitions will be ignored.
Similarly, other configuration managers can declare their own options for services to customize.

## Composition and Ownership {#modular-service-composition}

Compared to traditional services, modular services are inherently more composable, by virtue of being modules and receiving a user-provided name when imported.
However, composition can not end there, because services need to be able to interact with each other.
This can be achieved in two ways:
1. Users can link services together by providing the necessary NixOS configuration.
2. Services can be compositions of other services.

These aren't mutually exclusive. In fact, it is a good practice when developing services to first write them as individual services, and then compose them into a higher-level composition. Each of these services is a valid modular service, including their composition.

## Migration {#modular-service-migration}

Many services could be migrated to the modular service system, but even when the modular service system is mature, it is not necessary to migrate all services.
For instance, many system-wide services are a mandatory part of a desktop system, and it doesn't make sense to have multiple instances of them.
Moving their logic into separate Nix files may still be beneficial for the efficient evaluation of configurations that don't use those services, but that is a rather minor benefit, unless modular services potentially become the standard way to define services.

<!-- TODO example of a single-instance service -->

## Portable Service Options {#modular-service-options-portable}

```{=include=} options
id-prefix: service-opt-
list-id: service-options
source: @PORTABLE_SERVICE_OPTIONS@
```

## Systemd-specific Service Options {#modular-service-options-systemd}

```{=include=} options
id-prefix: systemd-service-opt-
list-id: systemd-service-options
source: @SYSTEMD_SERVICE_OPTIONS@
```

[module]: https://nixos.org/manual/nixpkgs/stable/index.html#module-system
<!-- TODO: more anchors -->
[`attrsOf`]: #sec-option-types-composed
[`submodule`]: #sec-option-types-submodule
18 changes: 18 additions & 0 deletions nixos/doc/manual/redirects.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@
"book-nixos-manual": [
"index.html#book-nixos-manual"
],
"modular-service-composition": [
"index.html#modular-service-composition"
],
"modular-service-migration": [
"index.html#modular-service-migration"
],
"modular-service-options-portable": [
"index.html#modular-service-options-portable"
],
"modular-service-options-systemd": [
"index.html#modular-service-options-systemd"
],
"modular-service-portability": [
"index.html#modular-service-portability"
],
"modular-services": [
"index.html#modular-services"
],
"module-services-anubis": [
"index.html#module-services-anubis"
],
Expand Down
26 changes: 1 addition & 25 deletions nixos/lib/utils.nix
Original file line number Diff line number Diff line change
Expand Up @@ -119,31 +119,7 @@ let
)
);

# Quotes an argument for use in Exec* service lines.
# systemd accepts "-quoted strings with escape sequences, toJSON produces
# a subset of these.
# Additionally we escape % to disallow expansion of % specifiers. Any lone ;
# in the input will be turned it ";" and thus lose its special meaning.
# Every $ is escaped to $$, this makes it unnecessary to disable environment
# substitution for the directive.
escapeSystemdExecArg =
arg:
let
s =
if isPath arg then
"${arg}"
else if isString arg then
arg
else if isInt arg || isFloat arg || isDerivation arg then
toString arg
else
throw "escapeSystemdExecArg only allows strings, paths, numbers and derivations";
in
replaceStrings [ "%" "$" ] [ "%%" "$$" ] (toJSON s);

# Quotes a list of arguments into a single string for use in a Exec*
# line.
escapeSystemdExecArgs = concatMapStringsSep " " escapeSystemdExecArg;
inherit (config.systemd.package.functions) escapeSystemdExecArg escapeSystemdExecArgs;

# Returns a system path for a given shell package
toShellPath =
Expand Down
4 changes: 3 additions & 1 deletion nixos/modules/misc/assertions.nix
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,7 @@
};

};
# impl of assertions is in <nixpkgs/nixos/modules/system/activation/top-level.nix>
# impl of assertions is in
# - <nixpkgs/nixos/modules/system/activation/top-level.nix>
# - <nixpkgs/nixos/modules/system/service/portable/lib.nix>
}
2 changes: 2 additions & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1797,6 +1797,8 @@
./system/boot/tmp.nix
./system/boot/uvesafb.nix
./system/etc/etc-activation.nix
./system/service/systemd/system.nix
./system/service/systemd/user.nix
./tasks/auto-upgrade.nix
./tasks/bcache.nix
./tasks/cpu-freq.nix
Expand Down
28 changes: 28 additions & 0 deletions nixos/modules/system/service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

# Modular Services

This directory defines a modular service infrastructure for NixOS.
See the [Modular Services chapter] in the manual [[source]](../../doc/manual/development/modular-services.md).

[Modular Services chapter]: https://nixos.org/manual/nixos/unstable/#modular-services

# Design decision log

- `system.services.<name>`. Alternatives considered
- `systemServices`: similar to does not allow importing a composition of services into `system`. 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 than `services.abstract`, but still weird

- No `daemon.*` options. https://github.com/NixOS/nixpkgs/pull/267111/files#r1723206521

- For now, do not add an `enable` option, 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)?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 to keep the distinction. I had cases where I wanted one or the other.


- Move all process options into a `process` option 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.nix` has `system` twice. Not great, but
- they have different meanings
1. These are system-provided modules, provided by the configuration manager
2. `systemd/system` configures SystemD _system units_.
- This reserves `modules/service` for actual service modules, at least until those are lifted out of NixOS, potentially

33 changes: 33 additions & 0 deletions nixos/modules/system/service/portable/lib.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{ lib, ... }:
let
inherit (lib) concatLists mapAttrsToList showOption;
in
rec {
flattenMapServicesConfigToList =
f: loc: config:
f loc config
++ concatLists (
mapAttrsToList (
k: v:
flattenMapServicesConfigToList f (
loc
++ [
"services"
k
]
) v
) config.services
);

getWarnings = flattenMapServicesConfigToList (
loc: config: map (msg: "in ${showOption loc}: ${msg}") config.warnings
);

getAssertions = flattenMapServicesConfigToList (
loc: config:
map (ass: {
message = "in ${showOption loc}: ${ass.message}";
assertion = ass.assertion;
}) config.assertions
);
}
63 changes: 63 additions & 0 deletions nixos/modules/system/service/portable/service.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
lib,
config,
options,
...
}:
let
inherit (lib) mkOption types;
pathOrStr = types.coercedTo types.path (x: "${x}") types.str;
program =
types.coercedTo (
types.package
// {
# require mainProgram for this conversion
check = v: v.type or null == "derivation" && v ? meta.mainProgram;
}
Comment on lines +12 to +16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: Putting this in a variable would improve readability (though also negligibly decrease performance)

) lib.getExe pathOrStr
// {
description = "main program, path or command";
descriptionClass = "conjunction";
};
in
{
# https://nixos.org/manual/nixos/unstable/#modular-services
_class = "service";
imports = [
../../../misc/assertions.nix
];
options = {
services = mkOption {
type = types.attrsOf (
types.submoduleWith {
modules = [
./service.nix
];
}
);
description = ''
A collection of [modular services](https://nixos.org/manual/nixos/unstable/#modular-services) that are configured in one go.

You could consider the sub-service relationship to be an ownership relation.
It **does not** automatically create any other relationship between services (e.g. systemd slices), unless perhaps such a behavior is explicitly defined and enabled in another option.
'';
default = { };
visible = "shallow";
};
process = {
executable = mkOption {
type = program;
description = ''
The path to the executable that will be run when the service is started.
'';
};
args = lib.mkOption {
type = types.listOf pathOrStr;
description = ''
Arguments to pass to the `executable`.
'';
default = [ ];
};
};
};
}
Loading
Loading