Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Config

The config key in a module is where options are assigned values. While options defines the schema, config is where the system actually gets configured. Most of what you write in a NixOS module — and almost everything in a user’s configuration.nix — lives under config.

Basic assignment

Assignments follow the option’s path directly:

{ pkgs, ... }:

{
  config = {
    networking.hostName = "myhost";
    time.timeZone = "Europe/London";
    environment.systemPackages = [ pkgs.git pkgs.vim ];
  };
}

When a module contains only config and no options or imports, the config wrapper can be omitted — the module system treats a bare attribute set as if it were config:

{ pkgs, ... }:

{
  networking.hostName = "myhost";
  environment.systemPackages = [ pkgs.git pkgs.vim ];
}

This shorthand is common in user configurations but can cause confusion when a module also declares options, so modules in shared libraries conventionally always use the explicit config = { ... } form.

Conditional config with mkIf

It is common for a module’s configuration to apply only when an enable option is set. lib.mkIf expresses this:

{ config, lib, pkgs, ... }:

{
  options.services.myapp.enable = lib.mkEnableOption "myapp";

  config = lib.mkIf config.services.myapp.enable {
    systemd.services.myapp = {
      description = "My Application";
      wantedBy    = [ "multi-user.target" ];
      serviceConfig.ExecStart = "${pkgs.myapp}/bin/myapp";
    };
  };
}

mkIf is lazy: the body is not evaluated at all when the condition is false. This avoids evaluation errors in the disabled branch and keeps the module system efficient.

mkIf vs if-then-else

Prefer mkIf over a bare if in config:

# Correct — lazy, integrates with module system priority
config = lib.mkIf condition { ... };

# Also works, but eager — the body is always evaluated
config = if condition then { ... } else { };

The difference matters when the disabled branch references options that may not be defined, or when you need mkMerge to combine conditional blocks.

mkMerge

lib.mkMerge combines multiple config attribute sets that would otherwise conflict:

config = lib.mkMerge [
  {
    environment.systemPackages = [ pkgs.curl ];
  }
  (lib.mkIf config.services.myapp.enable {
    environment.systemPackages = [ pkgs.myapp ];
    networking.firewall.allowedTCPPorts = [ 8080 ];
  })
];

Each element of the list is merged independently. This is the idiomatic way to express a module with several independent conditional blocks.

Priority: mkDefault, mkForce, and mkOverride

The module system allows multiple modules to assign the same option. For most mergeable types (lists, attribute sets) this just accumulates values. For scalar types (bool, str, int) there can only be one value — conflicts are resolved by priority.

Every assignment has a numeric priority. Lower numbers win. The module system defines named priorities:

FunctionPriorityPurpose
lib.mkDefault1000Provide a fallback that users can easily override
(no wrapper)100Normal assignment
lib.mkForce50Override user configuration
lib.mkOverride nnExplicit numeric priority

mkDefault

Use mkDefault for values that should be overridable:

config = lib.mkIf config.services.myapp.enable {
  networking.firewall.allowedTCPPorts = lib.mkDefault [ 8080 ];
};

A user can override this without needing mkForce:

networking.firewall.allowedTCPPorts = [ 9000 ];  # overrides the mkDefault above

mkForce

Use mkForce when a value must not be changed by users or other modules:

# Security module: always disable root login
config.services.openssh.settings.PermitRootLogin = lib.mkForce "no";

Overriding a mkForce value requires another mkForce with equal or lower priority, which makes the conflict explicit and intentional.

Conflict errors

If two modules assign the same scalar option at the same priority, the module system raises an error:

error: The option 'networking.hostName' has conflicting definition values:
  - In '/etc/nixos/configuration.nix': "host-a"
  - In '/etc/nixos/extra.nix': "host-b"

Resolve this by deciding which value should win and wrapping it in mkForce, or by moving the common configuration to a shared location.

Assertions and warnings

Modules can validate configuration and surface problems early.

assertions

assertions is a list of { assertion = bool; message = str; } records. Any false assertion aborts evaluation with the given message:

config = lib.mkIf config.services.myapp.enable {
  assertions = [
    {
      assertion = config.services.myapp.port != 80 || config.services.nginx.enable;
      message   = "myapp on port 80 requires nginx to be enabled.";
    }
  ];
};

warnings

warnings is a list of strings. Each string is printed as a warning during nixos-rebuild but does not abort:

config.warnings = lib.optional
  (config.services.myapp.dataDir == "/tmp")
  "services.myapp.dataDir is set to /tmp; data will not persist across reboots.";