How do I modularize configuration snippets to modules?

Hello everyone, is there any guided tutorials on configuring a NixOS system with flakes and modules? I’m new to NixOS, did some basic configuration amd then decided that I’d better organize all the things better with modules. However I couldn’t really understand how everything works and documents don’t have much examples. I also tried reading others’ config but many of them are way too complicated to refer to.

My current config is like:

Directory Structure
flake.nix

nixos:
    bootloader.nix             
    cache-servers.nix          
    configuration.nix          
    dae.nix                    
    disko-config.nix           
    display.nix                
    fonts.nix                  
    graphics-card.nix          
    hardware-configuration.nix 
    i18n.nix                   
    networking.nix             
    openssh.nix                
    packages-system.nix        
    users.nix                  

home-manager:
    fcitx5.nix        
    home.nix          
    packages-user.nix 

and for example, bootloader.nix contains:

{
  boot.loader = {
    grub = {
      enable = true;
      efiSupport = true;
      device = "nodev";
    };

    efi = {
      canTouchEfiVariables = true;
    };
  };
}

My questions: What kind of system config(boot? network? system packages? …) should be moved to modules and what should stay in each machine’s configuration.nix? How to turn config snippets like above example to a module and how to apply that module to my system config?

I’m currently single user on single machine, and won’t be managing multiple physical devices in the near future. I do want to ensure my setup is scalable of some sort. Also I’ve picked Snowfall Lib, for it looks promising.

I’d appreciate any suggestion or links to resources that may have answered my questions(I couldn’t find suitable tutorials)

This repo might be helpful

I should start out with saying that I have a bit of a distaste for the various NixOS frameworks. They attempt to abstract away the NixOS module system without eliminating it, forcing you to learn two abstraction systems at the same time and making the whole thing more confusing because it’s yet another step removed from the actual deployment. On top of all of that, they add a significant switching cost if you ever want to stop using a specific framework, add a layer of relying on third party maintenance that may or may not disappear, etc.

So overall I’d suggest just not touching those frameworks (including snowfall lib), at least until you’re comfortable enough with the NixOS module system to understand what the frameworks are doing. Once you have that, you can use a framework to abstract away that understanding if you so desire, but I’ve not felt a need to do so at all with NixOS personally.

It’s like with all software engineering, if you tried to learn React before you understood HTML you’d probably struggle. And then React only makes sense because JS lacks a good way to map data to UI changes without painful micro-management of the DOM - if you use one for any reason but solving such painpoints, using a framework probably means you’re lacking some fundamental underlying understanding.

This is a very generic question. The same question probably applies to all programming in languages where multi-file declarations exist. IME letting these things happen organically over time often results in the best organization; you’ll go through several cycles of refactoring and eventually arrive at something sane.

You might be afraid that you’ll be painting yourself into a corner, but refactoring NixOS modules is really easy, simple NixOS usecases end up with painfully simple code, and there is no state to be afraid of screwing up. You also appear to be working on personal configuration, so you have no API to be worrying about. There’s absolutely zero reason to worry about splitting your modules if you don’t know how you should split them yet.

So for simple system configs, most things can just be in a big configuration.nix, especially initially. As your config grows you’ll naturally find things that feel like they belong together, and you can then break those things out into separate files. I’ll give some examples from my personal experience after explaining the rest, but that’s just my experience - I think module organization is entirely a matter of taste.

To start out, I’d recommend creating just a single configuration.nix with your general system config, and then a separate directory for each machine you use. Give each machine directory a default.nix where you place the various configuration that should only occur on that machine, and put the hardware-configuration.nix that you generate for each machine on install in the same directory.

I’ll get to how you build this up into a set of configurations you can build in a sec (by answering your other question).

Funnily enough, that already is a module! If you’re using a flake, you can literally put that in the modules arg of the nixosSystem function:

# flake.nix
{
  # <imagine some kind of inputs>
  outputs = { nixpkgs, ... }: {
    nixosConfigurations.yourcomputer = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux"; # For example
      modules = [({
        boot.loader = {
          grub = {
            enable = true;
            efiSupport = true;
            device = "nodev";
          };

          efi = {
            canTouchEfiVariables = true;
          };
        };
      })];
    };
  };
}

Of course, it would be a bit messy to put all your modules in-line in your flake.nix like this (though at times it can be useful to do so, some people add their home-manager modules like this). So you can just write that exact code in a separate file - say ./boot.nix - and tell nixosSystem to load said file:

# flake.nix
{
  # <imagine some kind of inputs>
  outputs = { nixpkgs, ... }: {
    nixosConfigurations.yourcomputer = nixpkgs.lib.nixosSystem {
      system = "x86_64-linux"; # For example
      modules = [
        ./boot.nix
      ];
    };
  };
}

Modules aren’t magic - they are just attribute sets (JSON objects, if that helps you) that the NixOS module system merges together. There are some rules to this merge for what happens when options are declared multiple times (e.g. lists will be concatenated), but for the most part you don’t need to know much about it other than that it happens.

So, if we wanted to build the setup I outlined earlier, you could do this:

# flake.nix
{
  # <imagine some kind of inputs>
  outputs = { nixpkgs, ... }: {
    nixosConfigurations = {
      yourcomputer = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux"; # For example
        modules = [
          ./configuration.nix
          # When specifying a directory, NixOS will automatically read the
          # `default.nix` from it if it exists, similar to e.g.
          # `__init__.py` in the python world
          ./hosts/yourcomputer
          ./hosts/yourcomputer/hardware-configuration.nix
        ];
      };
      
      yourothercomputer = nixpkgs.lib.nixosSystem {
        system = "aarch64-linux";
        modules = [
          ./configuration.nix
          ./hosts/yourothercomputer
          ./hosts/yourothercomputer/hardware-configuration.nix
        ];
      };
    };
  };
}

To install yourothercomputer’s config, you could then just:

nixos-rebuild switch --flake .#yourothercomputer

It’d get tedious to have to duplicate all the modules you want to import between both machines, though. This is where you start using the NixOS module system. From a module, you can import other modules:

# configuration.nix
{
  imports = [
    ./boot.nix
  ];
}

This just instructs nix to merge together these two modules. In fact, we can use this to abstract away the need to put a reference to hardware-configuration.nix in flake.nix:

# hosts/yourothercomputer/default.nix
{
  imports = [
    ./hardware-configuration.nix
  ];
}

Now, sometimes you’ll find that a specific machine needs to deviate from something in the global configuration.nix, but you don’t want to make that one thing machine-specific across all machines to avoid code duplication. This is where you’ll need to use lib.mkDefault, lib.mkForce or lib.mkOverride:

# hosts/yourothercomputer/default.nix
{
  imports = [
    ./hardware-configuration.nix
  ];

  # Grub doesn't work on this motherboard
  boot.loader.grub.enable = lib.mkForce false;
  boot.loader.systemd-boot.enable = true;
}

And there we go, a fully functional, modular, multi-system NixOS config without any crazy frameworks! I’m sure you can see how you would port your current directory structure to this - and it’d be all organic and actually match how you understand the units of your configuration.

Looking at your directory structure, I’d personally place these files into the machine-specific directories:

  • disko-config.nix
    • Partition management will likely be system-specific for “normal” computers (i.e. not kubernetes cattle stuff)
  • hardware-configuration.nix
    • This is always going to be system-specific, by definition
  • networking.nix
    • Your link names will likely be system-specific because of predictable interface names, so unless you have no link-specific configuration this probably needs to be separate too

That’s just guessing contents, though, you’ll know better what actually is system-specific - and you might not even know until you get at least one other system.


For me personally, I’ve noticed breaking things out of configuration.nix that may be useful across multiple machines, but not necessarily all machines, is very useful. E.g., my nvidia configuration is a complete mess because nvidia is nvidia, and as such every nvidia system I will ever own will want a copy of it, but other machines of course will not.

Placing that in every machine-specific directory would be a lot of code duplication - so this should be factored out into a ./nvidia.nix, which specific systems can then add: imports = [../../nvidia.nix];.

I only own one nvidia machine currently, though, I’m sure in the future I’ll find some ways in which this is not yet good enough and that will force me to refactor some of it. Or maybe not, I’ll probably avoid buying nvidia in the future.

Other things like my wireguard config are shared across all machines, but annoyingly verbose, so I place them in a global file where they don’t disrupt the general flow of configuration.nix.

That said, my configuration.nix still contains a lot of stuff, at least for my personal devices. I’ve not felt much of a need to overcomplicate it - in fact, the stuff I did with my home-manager configuration is painfully overcomplicated and I will probably rewrite that from scratch at some point (and make it much more plain).

3 Likes

Thank you for your detailed reply! It’s really helpful to me, and untangled some concepts that I don’t understand.

Yeah, I made this post out of that, but I guess you’re right, as NixOS don’t have that much “debt” when refactoring.

True… I have been over-planning and I could agree with you point.

So I will just stick with my existing without-framework config instead of thinking about some re-organize this early. And your examples and experiences are of great help, thanks again!

1 Like