Modular Services

Revision 06cbfccb1935bc9adce73c0a1e9df44a4abeaf8e


Modular Services

Status: in development. This functionality is new in release 25.11, and significant changes should be expected. We’d love to hear your feedback in our matrix channel or at the tracking issue.

This repository carries the subsystem outside of nixpkgs so that it can move at its own pace and be consumed by NixOS, Home Manager and nix-darwin alike. The in-tree nixpkgs copy is disabled at evaluation time; see integrations/nixos/disable-upstream.nix.

In NixOS, services were traditionally 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, nix-darwin, nimi and finix.

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, on nix-darwin the module wrapping launchd, and on finix the module wrapping finit.

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.

The NixOS integration provided by this repository declares 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. 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:

{ config, ... }:
{
  system.services.my-service-instance = {
    imports = [ config.modularServices.some-application.default ];
    foo.settings = {
      # ...
    };
  };
}

config.modularServices.<pkg>.<svc> is how a NixOS configuration consumes a service: the service itself, plus the systemd-specific definitions that only this integration can supply. modularServices.<pkg> pkgs is that service on its own, for an integration whose service manager is not systemd. pkgs.<pkg>.services.* still resolves to the nixpkgs copy unless overlays.passthruServices is applied; see the README.

Motivation

Traditionally a NixOS service is a set of options in a module: one module declares services.foo.*, there is exactly one instance of it, and its name is fixed by whoever wrote the module. Such a service cannot be instantiated twice, cannot be extended without patching the module that declares it, and cannot be used outside NixOS.

A modular service is a module as a service, which makes it:

This is also the answer to “why not just ship a systemd unit file”. A unit file describes one service, for one service manager, in a format with no notion of options, types, defaults, or merging; it cannot be parameterized, imported twice under different names, or extended without editing it. A modular service is a module that produces such a unit, and going through the module system is what buys the four properties above. systemd-specific escapes remain available through the systemd option tree, guarded as shown in the section called “Portability”.

The portable option set is still small, as we try to find reusable abstractions. It describes how to start the service (process.argv), optionally how to reload it (process.reloadSignal, process.reloadCommand, and configData for files whose contents may change without a restart), and how the service signals readiness (notificationProtocol). Everything else – ordering, dependencies, restart policy, isolation – is left to the service manager, and on NixOS is reached through the systemd options.

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.

A module that only defines portable options is portable, but requires some translation layer to be picked up by a service manager:

# Non-module dependencies (`importApply`)
{ pkgs }:

# Service module
{ config, lib, ... }:
let
  cfg = config.foo;
  format = pkgs.formats.toml { };
in
{
  _class = "service";

  options.foo = {
    package = lib.mkPackageOption pkgs "foo" { };
    settings = lib.mkOption {
      type = format.type;
      default = { };
      description = "Configuration for `foo`, rendered to `foo.toml`.";
    };
  };

  config.process.argv = [
    (lib.getExe cfg.package)
    "--config"
    (format.generate "foo.toml" cfg.settings)
  ];
}

You can define process-manager-specific definitions in an optional way like:

{
  config,
  options,
  lib,
  ...
}:
{
  _class = "service";
  config = {
    process.argv = [ (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.

The services in this repository take the second route further, and keep the manager-specific definitions out of the service module entirely: they live in a variant alongside the integration that understands them, integrations/nixos/modular/<pkg>/<svc>/system.nix for NixOS. That leaves the service module in modular-services/ portable by construction rather than by convention, and it lets an integration refine a service it does not own.

Composition and Ownership

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

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.

Writing and Reviewing a Modular Service

For more details, refer to the contributor documentation in Writing and Reviewing Modular Services.

Portable Service Options

_module.args

Additional arguments passed to each module in addition to ones like lib, config, and pkgs, modulesPath.

This option is also available to all submodules. Submodules do not inherit args from their parent module, nor do they provide args to their parent module or sibling submodules. The sole exception to this is the argument name which is provided by parent modules to a submodule and contains the attribute name the submodule is bound to, or a unique generated name if it is not bound to an attribute.

Some arguments are already passed by default, of which the following cannot be changed with this option:

  • lib: The nixpkgs library.

  • config: The results of all options after merging the values from all modules together.

  • options: The options declared in all modules.

  • specialArgs: The specialArgs argument passed to evalModules.

  • All attributes of specialArgs

    Whereas option values can generally depend on other option values thanks to laziness, this does not apply to imports, which must be computed statically before anything else.

    For this reason, callers of the module system can provide specialArgs which are available during import resolution.

    For NixOS, specialArgs includes modulesPath, which allows you to import extra modules from the nixpkgs package tree without having to somehow make the module aware of the location of the nixpkgs or NixOS directories.

    { modulesPath, ... }: {
      imports = [
        (modulesPath + "/profiles/minimal.nix")
      ];
    }
    

For NixOS, the default value for this option includes at least this argument:

  • pkgs: The nixpkgs package set according to the nixpkgs.pkgs option.

Type: lazy attribute set of raw value

Default:

{ }

Declared by:

<nixpkgs/lib/modules.nix>
configData

Configuration data files for the service

These files are made available to the service and can be updated without restarting the service process, enabling configuration reloading. The service manager implementation determines how these files are exposed to the service (e.g., via a specific directory path). This path is available in the path sub-option for each configData.<name> entry.

This is particularly useful for services that support configuration reloading via signals (e.g., SIGHUP) or which pick up changes automatically, so that no downtime is required in order to reload the service.

Type: lazy attribute set of (submodule)

Default:

{ }

Example:

{
  "server.conf" = {
    text = ''
      port = 8080
      workers = 4
    '';
  };
  "ssl/cert.pem" = {
    source = ./cert.pem;
  };
}

Declared by:

lib/services/config-data.nix
configData.<name>.enable

Whether this configuration file should be generated. This option allows specific configuration files to be disabled.

Type: boolean

Default:

true

Declared by:

lib/services/config-data-item.nix
configData.<name>.name

Name of the configuration file (relative to the service’s configuration directory). Defaults to the attribute name.

Type: string

Declared by:

lib/services/config-data-item.nix
configData.<name>.path

The actual path where this configuration file will be available. This is determined by the service manager implementation.

On NixOS it is an absolute path. Other service managers may provide a relative path, in order to be unprivileged and/or relocatable.

Type: string (read only)

Declared by:

lib/services/config-data-item.nix
configData.<name>.source

Path of the source file.

Type: absolute path

Declared by:

lib/services/config-data-item.nix
configData.<name>.text

Text content of the configuration file.

Type: null or strings concatenated with “\n”

Default:

null

Declared by:

lib/services/config-data-item.nix
meta.maintainers

List of maintainers of each module. This option should be defined at most once per module.

The option value is not a list of maintainers, but an attribute set that maps module file names to lists of maintainers.

Type: list of lib.maintainers

Default:

[ ]

Example:

[ lib.maintainers.alice lib.maintainers.bob ]

Declared by:

lib/services/vendor/meta-maintainers.nix
meta.teams

List of team maintainers of each module. This option should be defined at most once per module.

Type: list of lib.teams

Default:

[ ]

Example:

[ lib.teams.acme lib.teams.haskell ]

Declared by:

lib/services/vendor/meta-maintainers.nix
notificationProtocol

Notification protocol that this service supports with the underlying service manager.

Type: submodule

Declared by:

lib/services/service.nix
notificationProtocol.s6

Whether to enable Whether the service supports s6-notify…

Type: boolean

Default:

false

Example:

true

Declared by:

lib/services/service.nix
notificationProtocol.systemd

Whether to enable Whether the service supports systemd-notify…

Type: boolean

Default:

false

Example:

true

Declared by:

lib/services/service.nix
process.argv

Command filename and arguments for starting this service. This is a raw command-line that should not contain any shell escaping. If expansion of environmental variables is required then use a shell script or importas from pkgs.execline.

When flags are set, the arguments rendered from them are merged into argv. See flags for how the two are ordered against each other.

Type: list of (string or absolute path convertible to it)

Default:

[ ]

Example:

[ (lib.getExe config.package) "--nobackground" ]

Declared by:

lib/services/service.nix
process.flagFormat

Function mapping flag names to option format specs for lib.cli.toCommandLine.

Receives the flag name and returns { option, sep, explicitBool, formatArg? }.

Type: function that evaluates to a(n) attribute set of anything

Default:

<function>

Example:

name: {
  option = name;
  sep = "=";
  explicitBool = false;
}

Declared by:

lib/services/service.nix
process.flags

Flags to pass to the service process. The key is the flag name (e.g. "--port"), the value is the flag value.

Each name = value pair is rendered via lib.cli.toCommandLine using flagFormat.

  • null: the flag is omitted (regardless of flagFormat)

  • bool: rendered per flagFormat.explicitBool

    • explicitBool = false (default): true emits the bare flag, false is omitted

    • explicitBool = true: both true and false are rendered as explicit arguments via flagFormat.formatArg

  • string / path / int: rendered as the option’s argument, joined to the option name per flagFormat.sep and stringified by flagFormat.formatArg

To pass the same flag multiple times, use the list form with repeated keys, e.g. [ { "--host" = "a"; } { "--host" = "b"; } ].

The rendered arguments are merged into argv, so argv and flags share a single lib.mkOrder space:

  • A flag with no ordering property of its own is placed at priority 1250, between lib.modules.defaultOrderPriority (1000, which is what an unadorned argv definition gets) and lib.mkAfter (1500). Plain flags therefore follow the command name and any other plain argv arguments.

  • lib.mkAfter on argv still lands after the flags, which is how trailing positional arguments are expressed.

  • lib.mkOrder on a flag is honoured verbatim against argv, so a sub-command can be placed between two groups of flags.

Because 1250 is substituted for flags that carry no ordering property, lib.mkOrder 1000 on a flag is indistinguishable from leaving that flag unadorned. To order a flag around plain argv entries, pick a priority next to 1000, such as 999 or 1001.

Type: attribute list of (null or boolean or signed integer or (string or absolute path convertible to it))

Default:

{ }

Example:

{
  "--port" = "8080";
  "--verbose" = true;
  # ordered ahead of the unadorned flags above
  "--config" = lib.mkOrder 1100 "/etc/foo.conf";
}
# or, for repeated flags:
[
  { "--host" = "localhost"; }
  { "--host" = "0.0.0.0"; }
]

Declared by:

lib/services/service.nix
process.reloadCommand

Command used for reloading in the underlying service manager to reload.

Type: null or string

Default:

null

Example:

"${pkgs.coreutils}/bin/kill -HUP $MAINPID"

Declared by:

lib/services/service.nix
process.reloadSignal

Configures the reload signal to send to the service manager.

Type: null or string

Default:

null

Example:

"HUP"

Declared by:

lib/services/service.nix
services

A collection of 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.

Type: attribute set of (submodule)

Default:

{ }

Declared by:

lib/services/service.nix

Systemd-specific Service Options

_module.args

Additional arguments passed to each module in addition to ones like lib, config, and pkgs, modulesPath.

This option is also available to all submodules. Submodules do not inherit args from their parent module, nor do they provide args to their parent module or sibling submodules. The sole exception to this is the argument name which is provided by parent modules to a submodule and contains the attribute name the submodule is bound to, or a unique generated name if it is not bound to an attribute.

Some arguments are already passed by default, of which the following cannot be changed with this option:

  • lib: The nixpkgs library.

  • config: The results of all options after merging the values from all modules together.

  • options: The options declared in all modules.

  • specialArgs: The specialArgs argument passed to evalModules.

  • All attributes of specialArgs

    Whereas option values can generally depend on other option values thanks to laziness, this does not apply to imports, which must be computed statically before anything else.

    For this reason, callers of the module system can provide specialArgs which are available during import resolution.

    For NixOS, specialArgs includes modulesPath, which allows you to import extra modules from the nixpkgs package tree without having to somehow make the module aware of the location of the nixpkgs or NixOS directories.

    { modulesPath, ... }: {
      imports = [
        (modulesPath + "/profiles/minimal.nix")
      ];
    }
    

For NixOS, the default value for this option includes at least this argument:

  • pkgs: The nixpkgs package set according to the nixpkgs.pkgs option.

Type: lazy attribute set of raw value

Default:

{ }

Declared by:

<nixpkgs/lib/modules.nix>
systemd.lib

Library functions for working with systemd services.

Available functions:

  • escapeSystemdExecArgs: Escapes a list of arguments for use in ExecStart. Prevents systemd’s specifier (%) and variable ($) substitution by escaping them to %% and $$ respectively.

    Example: escapeSystemdExecArgs [ "/bin/echo" "Unit %n" ] produces "/bin/echo" "Unit %%n"

Type: lazy attribute set of raw value (read only)

Default:

{ }

Declared by:

integrations/nixos/systemd/service.nix
systemd.mainExecReload

Main command line for systemd’s ExecReload with systemd’s specifier and environment variable substitution enabled.

This option sets the primary ExecReload entry, and is the way to extend the command line derived from process.reloadCommand.

This option allows you to use systemd specifiers like %n (unit name), %i (instance), %t (runtime directory), and environment variables using ${VAR} syntax in your command line.

By default, it is set to process.reloadCommand. Because process.reloadCommand is already a command line (not an argument list), it is used verbatim so that references like $MAINPID are preserved.

When process.reloadCommand is unset, this option is null and no ExecReload is emitted; a service may then set systemd.service.serviceConfig.ExecReload itself.

To extend process.reloadCommand with systemd specifiers, you can append to the command line:

systemd.mainExecReload =
  config.process.reloadCommand + " --systemd-unit %n";

This pattern allows you to pass the unit name (or other systemd specifiers) as additional arguments.

See systemd.service(5) (section “COMMAND LINES”) for details on variable substitution and systemd.unit(5) (section “SPECIFIERS”) for available specifiers like %n, %i, %t.

Type: null or string

Default:

config.process.reloadCommand

Declared by:

integrations/nixos/systemd/service.nix
systemd.mainExecStart

Main command line for systemd’s ExecStart with systemd’s specifier and environment variable substitution enabled.

This option sets the primary ExecStart entry. Additional ExecStart entries can be added via systemd.service.serviceConfig.ExecStart with lib.mkBefore or lib.mkAfter.

This option allows you to use systemd specifiers like %n (unit name), %i (instance), %t (runtime directory), and environment variables using ${VAR} syntax in your command line.

By default, this is set to the escaped version of process.argv to prevent systemd substitution. Set this option explicitly to enable systemd’s substitution features.

To extend process.argv with systemd specifiers, you can append to the escaped arguments:

systemd.mainExecStart =
  config.systemd.lib.escapeSystemdExecArgs config.process.argv + " --systemd-unit %n";

This pattern allows you to pass the unit name (or other systemd specifiers) as additional arguments while keeping the base command from process.argv properly escaped.

See systemd.service(5) (section “COMMAND LINES”) for details on variable substitution and systemd.unit(5) (section “SPECIFIERS”) for available specifiers like %n, %i, %t.

Type: string

Default:

config.systemd.lib.escapeSystemdExecArgs config.process.argv

Declared by:

integrations/nixos/systemd/service.nix
systemd.service

Alias of systemd.services."".

Type: submodule

Declared by:

integrations/nixos/systemd/service.nix
systemd.services

This module configures systemd services, with the notable difference that their unit names will be prefixed with the abstract service name.

This option’s value is not suitable for reading, but you can define a module here that interacts with just the unit configuration in the host system configuration.

Note that this option contains deferred modules. This means that the module has not been combined with the system configuration yet, no values can be read from this option. What you can do instead is define a module that reads from the module arguments (such as config) that are available when the module is merged into the system configuration.

Type: lazy attribute set of module

Default:

{ }

Declared by:

integrations/nixos/systemd/service.nix
systemd.socket

Alias of systemd.sockets."".

Type: submodule

Declared by:

integrations/nixos/systemd/service.nix
systemd.sockets

Declares systemd socket units. Names will be prefixed by the service name / path.

See systemd.services.

Type: lazy attribute set of module

Default:

{ }

Declared by:

integrations/nixos/systemd/service.nix

Writing and Reviewing Modular Services

Status

Modular Services are, as of writing, a new feature with support in NixOS. It is in development, and be considerate of the fact that the intermediate outcome of RFC 163 is that we should try a module-based approach to portable services; it is not yet a widely agreed upon solution.

Relation to NixOS Modules

  • A modular service is not a replacement for a NixOS module, but may be in the future.

  • Using a modular service to implement a NixOS module is an expected use case, but exposes the NixOS module to a degree of uncertainty that is not acceptable for widely used modules yet.

Maintainership

If you contribute a modular service, you must mark yourself as maintainer of the modular service. The maintainership of a modular service does not need to be the same as the maintainership of a NixOS module. If you are not a maintainer of the NixOS module, you should offer to join the NixOS module’s meta.maintainers team, so that you are included in reviews and discussions, most of which also affect the modular service. The NixOS module maintainers have no obligation towards the modular service, except perhaps to notify you if they notice that the modular service breaks.

Minimum Standard

Modular services MUST be accompanied by a VM test that exercises the modular service, in at least one integration under integrations/.

Modular services MUST have a meta.maintainers module attribute that lists the maintainers of the modular service.

Reviewing Modular Services

When reviewing a modular service, you should check the following. Details and rationale are provided below.

- [ ] Has a VM test, in every integration it claims to support (at minimum `integrations/nixos/tests/packages/`)
- [ ] Registered in that integration's `tests/default.nix`
- [ ] Has a `meta.maintainers` attribute
- [ ] Systemd-specific definitions live in a NixOS variant, not in the service itself, to promote portability.
- [ ] `_class = "service"`
- [ ] Imports nothing from `lib/services`, so that it stays integration-agnostic
- [ ] Has an entry in `modular-services/default.nix` whose `<ns>.package` default comes from the providing package
- [ ] Is the modular services infrastructure sufficient for this service? If one or more features are not covered, comment in https://github.com/NixOS/nixpkgs/issues/428084
- [ ] Has been added to `doc/registry.nix` (enforced by `checks.docs-registry-complete`)

Details

VM test

For NixOS, add the test to integrations/nixos/tests/packages/ and register it in integrations/nixos/tests/default.nix; the surrounding tests there are worked examples. A test file imports config.modularServices.<pkg>.<svc> into the service, so that it exercises the same variant a NixOS configuration gets. Best practices: keep tests minimal and focused (boot a VM, enable the service, and assert a basic request succeeds). For general guidance, see the NixOS Tests chapter.

Integration-specific definitions

A service module under modular-services/ declares what the service is: its options, and process.argv. Anything that only one service manager understands – unit dependencies, serviceConfig, credentials – belongs to a variant of that service, in the integration that understands it. For NixOS that is integrations/nixos/modular/<pkg>/<svc>/, a pair of files: default.nix imports the pure module out of modularServices.<pkg>, and system.nix adds the systemd definitions. Register the pair in integrations/nixos/modular/default.nix, as a path rather than an import, so that the variant keeps its own file attribution; checks.nixos-modular-variants asserts that.

Keeping the two apart is what lets a service be loaded into a configuration manager that has no systemd option tree at all. A service that must vary its own definitions per manager can still do so inline, with lib.optionalAttrs (options ? systemd); see Portability.

_class = "service"

A _class declaration ensures a clear error when the module is accidentally imported into a configuration that isn’t a modular service, such as a NixOS configuration.

Provide it as the first attribute in the module:

# Non-module dependencies (`importApply`)
{ writeScript, runtimeShell }:

# Service module
{ lib, config, ... }:
{
  _class = "service";

  options = {
    # ...
  };
  config = {
    # ...
  };
}

Overriding the package default

The package option of a service must default to the package that provides the service. Otherwise, since some packages are defined by an override, the modular service would launch a wrong package, if it builds at all.

In this repository a service module is registered in modular-services/default.nix, which supplies both the importApply of its non-module dependencies and that default:

{
  example =
    pkgs:
    {
      imports = [ (importApply ./example/service.nix { inherit (pkgs) formats; }) ];
      example.package = lib.mkDefault pkgs.example;
    };
}

lib.mkDefault is used throughout, rather than the unpriorised definition nixpkgs uses in passthru, so that a configuration can set the package without lib.mkForce.

In nixpkgs the equivalent lives in the package’s passthru.services, and must use finalAttrs.finalPackage so that overrides propagate. That form is still what a service upstreamed into nixpkgs needs. If it is not possible, or if the module is not represented by a single package, consider exposing the modular service directly by file path only.

Flake attributes

Everything this repository offers, flake or no flake. flake.nix adds no output of its own: it pins nixpkgs through flake.lock and keys the per-system outputs by system, so calling default.nix directly gives the same surface.

This chapter is generated from those outputs, and checks.docs-outputs-complete asserts that every one of them is described here.

Whatever the system

A flake publishes these unkeyed. None of them evaluates a package set, so a configuration that imports one pulls in no second nixpkgs.

attributewhat
lib.mkComplianceSuiteThe integration-agnostic compliance suite, for a package set.
lib.serviceslib.servicesFor against the lib this repository was called with.
lib.servicesForThe portable layer against a caller-supplied lib, and the entry point for a new integration.
modularServices.<pkg>modularServices.<pkg> pkgs yields the service itself, naming no service manager. A NixOS configuration imports config.modularServices.<pkg>.<svc> instead, which is this plus the systemd definitions the integration adds. pkgs.<pkg>.services.* still resolves to the nixpkgs copy unless overlays.passthruServices is applied. Listed under Service modules.
nixosModules.defaultThis repository’s implementation, plus the disable of the nixpkgs copy. The one to import.
nixosModules.disableUpstreamJust the disable, without the implementation.
nixosModules.documentationReplacement for the option-documentation registry that the disable removes.
nixosModules.systemServicesJust the systemd implementation, without the disable.
overlays.defaultAdds pkgs.modularServices.*. Overrides nothing, so no rebuilds.
overlays.passthruServicesOpt-in: repoints pkgs.<pkg>.services.* at this repository. Excludes php, which regenerates its own passthru.

For one system

These are produced for the system default.nix was called with, which is what a flake keys as checks.<system>, packages.<system> and so on.

attributewhat
checks.<name>Every test, integration and repo-level alike. kind splits the CI matrix: an eval check builds on any runner, a vm check needs /dev/kvm. integration is the directory under integrations/ the check came from, or repo for the checks that belong to no single one. Listed under Checks.
ci.matrixThe GitHub Actions job matrix. Consumed only by the workflow.
devShells.defaultnixfmt-tree and jq.
formatternixfmt-tree, so nix fmt formats the tree.
packages.docsThis manual.

Service modules

modularServices.<pkg> pkgs yields the service itself, naming no service manager. A NixOS configuration imports config.modularServices.<pkg>.<svc> instead, which is this plus the systemd definitions the integration adds. pkgs.<pkg>.services.* still resolves to the nixpkgs copy unless overlays.passthruServices is applied.

  • modularServices.autopush-rs-autoconnect

  • modularServices.autopush-rs-autoendpoint

  • modularServices.easytier

  • modularServices.ghostunnel

  • modularServices.git-pages

  • modularServices.holo-daemon

  • modularServices.ktls-utils

  • modularServices.php

  • modularServices.snid

Checks

Every test, integration and repo-level alike. kind splits the CI matrix: an eval check builds on any runner, a vm check needs /dev/kvm. integration is the directory under integrations/ the check came from, or repo for the checks that belong to no single one.

checkkindintegration
disable-keys-existevalrepo
docsevalrepo
docs-outputs-completeevalrepo
docs-registry-completeevalrepo
lib-evalevalrepo
nixos-compliance-basic-argvvmnixos
nixos-compliance-evalevalnixos
nixos-compliance-reloadvmnixos
nixos-compliance-sub-servicesvmnixos
nixos-compliance-systemd-evalevalnixos
nixos-disable-proofevalnixos
nixos-etcvmnixos
nixos-modular-variantsevalnixos
nixos-pkg-autopush-rsvmnixos
nixos-pkg-easytiervmnixos
nixos-pkg-ghostunnelvmnixos
nixos-pkg-git-pagesvmnixos
nixos-pkg-holo-daemonvmnixos
nixos-pkg-php-fpmvmnixos
nixos-pkg-snidvmnixos
nixos-pkg-tlshdvmnixos
nixos-unitsevalnixos
non-flake-consumerevalrepo

The Portable Layer

lib.services.importService

The portable service base, service.nix, with its non-module dependency applied: importService { inherit pkgs; } is a module. configure loads it by default; pass it, or a copy pinned from another revision, as baseModules to choose the base explicitly.

lib.services.importService :: { pkgs :: AttrSet } -> Module

lib.services.configure

Entrypoint for integrating modular services into a containing module system.

Each containing system (NixOS, …) calls configure to obtain a serviceSubmodule type for its services option. The returned submodule includes the portable service base and any service-manager-specific modules passed via extraRootModules.

Implementing for a new integration (e.g. home-manager, nix-darwin):

An integration lives in integrations/<name>/ and provides exactly four files. See integrations/README.md for the full contract; this docstring covers the one that calls configure.

integrations/<name>/default.nix is the module that a user of that configuration system imports. It declares the services option in terms of configure, and translates the resulting service tree into whatever the integration’s service manager consumes:

# integrations/darwin/default.nix
{ lib, config, pkgs, ... }:
let
  portable-lib = import ../../lib/services { inherit lib; };

  modularServiceConfiguration = portable-lib.configure {
    serviceManagerPkgs = pkgs;
    # To load a different portable service base, set `baseModules` instead:
    #   baseModules = [ (portable-lib.importService { inherit pkgs; }) ];
    extraRootModules = [
      ./launchd/service.nix    # launchd-specific options (plist generation, etc.)
    ];
  };
in
{
  _class = "darwin";

  imports = [ ./disable-upstream.nix ];

  options.services = lib.mkOption {
    type = lib.types.attrsOf modularServiceConfiguration.serviceSubmodule;
    default = { };
  };

  config = {
    # Convert service tree -> launchd plists, assertions, etc.
    # (analogous to how NixOS converts to systemd units)
    launchd.agents = ...;
    assertions = ...;
    warnings = ...;
  };
}

The remaining three files are disable-upstream.nix (eval-time removal of that configuration system’s in-tree copy, if it has one), lib.nix ({ evalSystem, runTest, ... }: how to evaluate and test there), and tests/default.nix ({ <name> = { kind = "eval" | "vm"; drv; }; }). ci/tests.nix discovers integrations/ from the filesystem, so an integration that honours the contract is picked up by checks, nix flake check and CI with no further wiring.

lib.services.configure :: AttrSet -> { serviceSubmodule :: SubmoduleType }

Inputs

serviceManagerPkgs

1. A Nixpkgs instance used for built-in logic such as converting configData.<path>.text to a store path. Required unless baseModules is set.

baseModules

2. Modules that replace the portable service base. They are loaded into the “root” service submodule and must handle propagation to sub-services themselves. Defaults to this repository’s portable service base, importService { pkgs = serviceManagerPkgs; }. Set it to supply a different one, for example a service.nix pinned from another revision.

extraRootModules

3. Modules to be loaded into the “root” service submodule, but not into its sub-services. That’s the modules’ own responsibility. Typically contains service-manager-specific option modules (e.g. systemd unit options, launchd plist options).

extraRootSpecialArgs

4. Fixed module arguments provided alongside extraRootModules.

Output

An attribute set.

serviceSubmodule: a Module System option type which is a submodule with the portable modules and this function’s inputs loaded into it.