Day 18 — Modules as consumer

Updated

July 30, 2026

Day 18 — Modules as consumer

Stage II · ~4h (theory-heavy)
Goal: Use the NixOS module system as a consumer—read options and source for real modules, apply mkIf / mkDefault concepts correctly, and change behavior via options rather than forking nixpkgs.

Multiple NixOS modules merge into one system config

Module merge diagram

Why this day exists

Copy-pasting entire module files into your repo is a maintenance trap. Power users set options, import small local modules, and only override when necessary. That skill starts with reading.


Theory 1 — A module is a function (usually)

{ config, lib, pkgs, ... }: {
  options = { /* declarations */ };
  config = { /* implementations */ };
}
Argument Role
config Final merged configuration (lazy)
lib Library + mkOption, mkIf, …
pkgs Package set for this system
Other module args

Some files are plain attrsets; most nixpkgs modules use the function form.


Theory 2 — Options vs config

Side Meaning
options Schema: what may be set, types, defaults, descriptions
config Values and implementations that produce system artifacts

As a consumer you mostly set option values:

{
  services.openssh.enable = true;
  networking.firewall.allowedTCPPorts = [ 22 ];
}

You are writing config fragments that merge into the global config.


Theory 3 — Merge semantics (intuition)

Multiple modules set the same option; the module system merges by type:

Type vibe Merge idea
Boolean Priority rules / any true patterns depend on option
List Concatenate
Attrset Deep merge of attributes
Exclusive values Conflict error or priority

This is why shallow // (Day 3) is not how NixOS config works.


Theory 4 — lib.mkIf

{ config, lib, ... }: {
  config = lib.mkIf config.services.foo.enable {
    environment.systemPackages = [ /* … */ ];
    networking.firewall.allowedTCPPorts = [ 8080 ];
  };
}

mkIf cond value includes value only when cond is true—without breaking merge when false (unlike a raw if that might remove structure incorrectly in some patterns).

Consumer pattern: you rarely write mkIf until Day 19; you benefit from modules that use it when you set enable = true.


Theory 5 — lib.mkDefault and priority

services.openssh.enable = lib.mkDefault true;
Helper Idea
mkDefault Low priority default; easy to override
mkForce High priority; stomps others
mkOverride n Numeric priority control

Consumer rule: prefer ordinary assignment first. Use mkForce only when you understand why a default wins.

# last resort
networking.hostName = lib.mkForce "lab";

Theory 6 — How to read a nixpkgs module

Step path

  1. Find option on search.nixos.org/options
  2. Note default and type
  3. Open “Declared in” source file
  4. Skim options then config = mkIf cfg.enable

Local source on a NixOS system

# approximate — paths vary by version
nix eval --raw 'nixpkgs#path'
# then browse nixos/modules/...

Or:

nix repl
:lf nixpkgs
# explore; or use ripgrep in a checkout

What to extract mentally

Question Why
What does enable turn on? Side effects
Which ports open? Firewall
Which user created? Permissions
What state dir? Backup/persist
Hardening defaults? Security

Theory 7 — Consumer anti-patterns

Anti-pattern Prefer
Vendor-copy whole module Set options; small local module
Patch store files Declarative options / overlays later
mkForce everything Understand merge
Ignore option descriptions Read them
Edit generated unit files in /etc Change Nix; rebuild

Theory 8 — imports as composition

{
  imports = [
    ./hardware-configuration.nix
    ./modules/ssh.nix
    ./modules/packages.nix
  ];
}

Each import is a module. Order rarely matters for merges (priorities do). Day 19 authors one.


Worked examples bank

Example A — Read openssh options

Conceptually you will find:

options.services.openssh.enable = mkEnableOption "…";
config = mkIf cfg.enable {};

Set as consumer:

services.openssh.enable = true;
services.openssh.settings.PasswordAuthentication = false;

Example B — Override a default carefully

# module might mkDefault some package set
# you add:
environment.systemPackages = [ pkgs.htop ];
# lists merge — usually no mkForce needed

Example C — When mkForce appears in blogs

Often silencing a conflict:

boot.kernelPackages = lib.mkForce pkgs.linuxPackages_latest;

Use sparingly; document why.


Lab 1 — Study three modules

Pick three from:

  • services.openssh
  • services.timesyncd or time
  • programs.git
  • networking.firewall
  • services.nginx (even if unused)

For each, fill:

Module enable option 2 interesting options One side effect

Keep notes in ~/nixos-config/docs/modules-study.md.


Lab 2 — Change behavior via options only

Without forking module source:

  1. Tighten or loosen one SSH setting
  2. Change timezone or NTP-related option
  3. Adjust firewall ports

Rebuild; verify with systemctl / ss / ssh.


Lab 3 — Trace an option to source

Using search.nixos.org or local nixpkgs:

  1. Choose networking.firewall.enable
  2. Find declaring file
  3. Read how firewall backend is selected

Write five lines on what enable = true actually does.


Lab 4 — Priority experiment (safe)

In a throwaway module snippet:

{ lib, ... }: {
  networking.hostName = lib.mkDefault "default-name";
}

And in main config:

networking.hostName = "lab";

Confirm final hostname is lab. Then reverse priorities with mkForce experimentally and reset to clean state.


Lab 5 — Consumer checklist

Before inventing a custom module tomorrow, ask:

  1. Does an option already exist?
  2. Can two options combine?
  3. Am I fighting a default that mkDefault set?

Common gotchas

Symptom Theory
Option does not exist Typos / wrong release
Infinite recursion config referring badly in custom modules
Silent override Priorities / mkForce elsewhere
Copied outdated module Diverges from nixpkgs
mkIf confusion Study enable patterns first

Checkpoint

  • Three modules studied with notes
  • Behavior changed via options only
  • Can explain options vs config
  • Can explain mkIf / mkDefault roles
  • Avoided forking nixpkgs modules

Commit

cd ~/nixos-config
git add .
git commit -m "day18: modules as consumer mkIf mkDefault literacy"

Tomorrow

Day 19 — First custom module. Extract spaghetti into options + config, imports, and one assertion.