Skip to content

Service Patterns

Overview

Most NixOS daemons are not hand-written unit files. They are modules under services.* that expose an enable flag (and settings), then—when enabled—define concrete systemd.services / timers / sockets, plus supporting config such as firewall ports, system users, and files under /etc. Prefer those high-level options when nixpkgs already ships a module; drop to raw systemd.* only when you need control the module does not offer.

Details

The usual module shape. A service module binds cfg = config.services.foo, declares options under options.services.foo, and wraps definitions in config = lib.mkIf cfg.enable { … }. Inside that mkIf, the module typically sets systemd.services.foo (and maybe timers or sockets), and may also touch networking.firewall, users.users, environment.etc, systemd.tmpfiles, and state under /var/lib. Conditioning on enable keeps unused services out of the evaluation result—see mkIf, mkMerge, mkOrder.

High-level services.* first. Search NixOS options for services. before writing units. Upstream modules already wire package selection, config files, users, and units. Use systemd.services.<name> for one-offs or overrides when no suitable module exists; see systemd integration.

Related option clusters.

Concern Typical options
Process / unit systemd.services.<name>, timers, sockets
Network exposure networking.firewall.allowedTCPPorts (and UDP / interfaces) — networking
Identity users.users.<name>.isSystemUser (and groups)
Runtime layout systemd.tmpfiles, state dirs under /var/lib
Static config files environment.etc

Escaping Exec* interpolations. systemd substitutes % specifiers and $ / ${…} environment forms, and splits on whitespace. Arguments you interpolate into ExecStart (and other Exec* lines)—especially from user-facing option lists—should go through utils.escapeSystemdExecArgs (or utils.escapeSystemdExecArg). The NixOS manual covers this under writing modules; do not disable environment substitution when using those helpers.

Activation restarts units. After evaluation and build, switch-to-configuration (via nixos-rebuild switch) reconciles the new generation with running systemd state: changed units are started, stopped, restarted, or reloaded as needed. One-shot setup still runs through the activation script; ongoing lifecycle stays with systemd.

Examples

Prefer an existing module when one exists:

{ ... }: {
  services.openssh.enable = true;
}

Minimal invented services.hello-style sketch (illustrative only—not a full production module):

{ config, lib, pkgs, ... }:
let
  cfg = config.services.hello;
in
{
  options.services.hello = {
    enable = lib.mkEnableOption "the hello demo service";
  };

  config = lib.mkIf cfg.enable {
    systemd.services.hello = {
      description = "Demo hello oneshot";
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        Type = "oneshot";
        ExecStart = "${pkgs.hello}/bin/hello";
      };
    };
  };
}

For dynamic args, pass them through escaping (module argument utils):

{ config, pkgs, utils, ... }:
# …
serviceConfig.ExecStart = ''
  ${pkgs.hello}/bin/hello ${utils.escapeSystemdExecArgs cfg.extraArgs}
'';

Concrete enable/settings for popular daemons: common service examples.

References

See also