custom-options.md

Custom options

Before this, you'll want

  1. How NixOS modules fit together
Modules24 min

A useful module leaves the small choices to the machine using it. We’re going to build one that lets another file write only this:

module.nixtry changing itModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.

The module will decide what those settings mean, reject nonsense, and turn the answer into real NixOS configuration.

Start with the two halves

Every module you have written so far only set options that NixOS had already declared. One kind of line, all the way down.

A module that brings its own options has two kinds, and from the outside they look identical. services.my-program.port = … could be this file declaring that the option exists, or it choosing a value for one. So the module system asks you to say which. options holds what another file may choose. config holds what this module adds to the machine once every file’s choices have been merged.

That split is also why config stops being optional. While a module only answers, NixOS lets you drop the wrapper and reads the whole file as config. The moment options appears at the top level, that shorthand is off for the entire file, and a bare networking.firewall beside it stops being a setting.

It will tell you so, and the fix is always the same: move it down into config. The module below has just grown its first declaration, and the line underneath it has not caught up.

GoalThe firewall line stopped being a setting when the declaration arrived. Put it back where settings go.

  • Move networking.firewall.enable down under a config half
module.nixModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.

Everything else in this lesson goes in one half or the other.

Declare the switch

Nearly every service starts with the same option: an off-by-default boolean that the whole module hangs on. lib.mkEnableOption writes that one for you. It takes the words that finish the sentence “Whether to enable …”:

ModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.
{ lib, ... }:

{
  options = {
    services.tea-kettle.enable = lib.mkEnableOption "the tea kettle";
  };

  config = { };
}

That single line declares a bool, defaults it to false, and writes the documentation. Now do the same for our program, and put it under one path: related options sharing a namespace is what keeps a service’s settings from scattering across the machine.

GoalDeclare services.my-program.enable with the label "my program service".

  • Declare the enable option
module.nixModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.

mkEnableOption is the shorthand. Everything it fills in silently is what the next step writes out by hand.

Give the port rules

A port has more to say than yes or no, so it gets the full form. lib.mkOption takes an attribute set describing the choice:

ModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.
{ lib, ... }:

{
  options = {
    services.tea-kettle.greeting = lib.mkOption {
      type = lib.types.str;
      default = "hello";
      example = "good evening";
      description = "Line the kettle prints when it boils.";
    };
  };

  config = { };
}

type is the gate every value has to pass. default is what the machine gets when nobody chooses. example and description are for the person reading the generated documentation. Write the same four for port.

GoalDeclare port as a real TCP port with default 3000, example 8080, and a description.

  • Declare port with lib.mkOption
  • Use the lib.types.port type
  • Set the default to 3000
  • Set the example to 8080
  • Add a description
module.nixModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.

Use lib.types.port, not a plain integer. It refuses values below zero and above 65535 before your program gets anywhere near them, so a typo becomes a build error instead of a service that will not start.

default and example are not the same promise

Two of those four fields hold a port number, and only one of them is a value the machine will ever use.

PickNobody sets services.my-program.port. Which port does the machine end up with?

default changes the configuration. example never does: it appears in the generated documentation and nowhere else. An option declared with neither is the third answer, and asking for its value is an error until somebody sets it.

Turn choices into configuration

Declaring an option changes nothing on its own. The config half has to read the merged answer back and do something with it, and it reads it from config, the same argument you met when a firewall rule watched openssh. Every module that does this gives that path a short name first:

ModuleA generic Nix module, checked with the module system but without NixOS or Home Manager options.
{ config, lib, ... }:

let
  cfg = config.services.tea-kettle;
in
{
  options = { };

  config = { };
}

cfg is the answer after every file has had its say, not a copy of whatever one of them happened to write. Below, use it with lib.optional, the helper from the lib lesson that produces a one-item list when its condition is true and an empty one when it is false, so the port is open only while the switch is on.

GoalOpen the selected port only while my-program is enabled.

  • Use cfg.enable and cfg.port to build the firewall list
configuration.nixNixOS moduleA NixOS module, checked against real NixOS options and evaluated by the NixOS module system.

That is a complete module. It declares a small set of choices, and turns the answer into ordinary NixOS options that the rest of the machine already knows how to read.

Put the module in its own file

Now do the real two-file move. The workbench evaluates both files together with the actual module system. Add the missing port declaration to my-program.nix, then configure it from host.nix.

Try itDeclare port as lib.types.port, then enable my-program on port 8080.

my-program.nixincludedNixOS moduleA NixOS module, checked against real NixOS options and evaluated by the NixOS module system.
host.nixincluded

evaluated system configuration

The option file owns the rules and the effect. The host file only chooses values. Neither file reaches into the other one. The module system joins them, checks the values, and hands my-program.nix the final answer through config.

What to keep

  • Put the settings a caller may choose under options, and what follows from them under config.
  • Use mkEnableOption for the usual off-by-default switch.
  • Use mkOption to give every richer value a type, default, example, and description.
  • The type is a gate, and default is the only one of the four that reaches the machine.
  • Read merged values back through config, usually named cfg at the top of the file.
  • Keep declarations and their effect in one module, then set them from another.

Useful links

NORMALCOURSE IN BETA