Anatomy of a NixOS Config
Table of Contents
Overview and video
This article provides a structural overview on the anatomy of my personal nix configuration.
If you prefer, I also have a two part video on YouTube available that covers the same content and diagrams.
Design Concepts and Constraints
These are some self-imposed design constraints that I use to help ensure order and organization over time.
- Favor readable and intuitive factors over efficiency factors
- Strive for modular repeatability
- Support commonalities across to hosts and users
- Strict Core vs Optional tolerance
- If a config is present on all hosts or user homes then it could be considered core otherwise it is optional
That last point on core vs optional is more relevant once you understand the common structure used in the hosts and home elements, but for me it a big one for me.
In my mind, as soon as you make an exception to the rule, you'll be adding unneeded inconsistency to your configuration. It will defeat the entire purpose of having a core in the first place.
Structural Elements
The term "Nix-Config" refers to the overall configuration defined by this entire repository. The terms config and configuration are used throughout to refer to context-specific configurations with-in the broader umbrella of the Nix-Config.
flake.nix
This file is a central staging area that connects external dependency sources and the internal structural elements that are used throughout the config. It is the entry point to hosts and user home configurations for the entire Nix-Config.
External dependencies are specified in flake.nix
as inputs and can include URLs to sources like the official package management channels as well as other flakes that provide nix-specific flavors of package configurations or other utilities that we want to leverage, such as sops-nix for secrets management and nix-vim for a NixOS specific neovim distribution.
The rest of the flake.nix
file consists of outputs specifications that declare each host and user's home configurations. The elements that make up outputs in the Nix-Config are:
Some of these elements consist of their own substructures and vary greatly in terms of complexity. The following sections will go into further detail about each element and the end of the article wraps up the entire structure.
Custom Modules
Path: ./modules
Custom modules to enable special functionality for NixOS or Home-Manager oriented configurations. Modules created here should be written in a way that they could shared outside of your configuration. They should only enable you to more effectively change settings within your configurations opposed to defining settings themselves.
The modules are divided into NixOS and home-Manager directories to keep system or host level modules separate from user or home level modules.
NixOS Modules
Path: ./modules/nixos
Home Manager Modules
Path: ./modules/home-manager
Overlays
Path: ./overlays
The best way to think about overlays in my mind is that they are custom overrides, modifications, or patches to upstream packages that you pull in through the inputs.
For a very rudimentary example, let's say that for whatever reason there is a package that you use and there is some quirk with the release that doesn't work nicely with something else you use. You could tweak something in the source code so that it works nicely in your environment but you don't want to actually modify the source itself. Instead, you could write a patch just for your use case and apply it as an overlay to stabilize your system.
There are a lot of other use cases but I won't go into any more detail here. You can read more about them in the official Nixpkgs Manual.
The important part for our context here, is that should the need arise to use an overlay, there is a distinct place in the structure to keep it organized.
Custom Packages
Path: ./pkgs
This element houses custom packages. If you need a package for something and can't find the right solution in an existing package, you could write your own code, and add it to your environment from here.
Similar to custom modules, you would ideally write your code in a way that you could publish the package upstream to be included in an official channel for others to use, but until you do that you can house the code here in it's own location of the structure.
This doesn't have to be for your own personal code either, you could package someone elses public code for more convenient use in nix.
When custom packages are evaluated, they are added to the same pkgs
attribute set as the official Nixpkgs so that all of them can be passed as an argument and accessed through the various config fixes in the same manner. See the outputs section of flake.nix
as well as ./pkgs/default.nix
for more.
Scripts
Not included in diagram
Path: ./scripts
This is where you can store custom automation scripts. Ideally, most of these would eventually become nixified and move elsewhere in the structure as appropriate.
Formatter
Path: declared in flake.nix
This element simply provides a cli formatting utility to help ensure the config uses well formed nix expressions. This is particularly useful early on, if you don't have your primary editor setup to use linters and formatters.
The nixpkgs-fmt package used in this config is accessible using nix fmt
.
DevShell
Path: ./shell.nix
This is a custom nix-shell that includes any of the packages you may require for development tasks and bootstrapping the config on new hosts. This is particularly useful if you don't necessarily need the packages permanently installed or available in your everyday shell settings.
This eliminates the need to run nix-shell -p foo bar
when performing typical dev and admin tasks with the nix-config. For added convenience, the .envrc
file at the root of the config refers to the flake and nix will automatically instantiate the dev shell when you access the nix-config
directory.
Hosts
Path: ./hosts
This element contains all of the host or system-specific settings for each distinct system controlled by the configuration. Host configs handle package installation and user preferences that are widely available to users of the system. As new hosts are added to the config, each one will get it's own file here to define all of the nixos packages to install and configure for it, as well as which users to create.
The host configs here are accessible via sudo nixos-rebuild switch --flake .#<host>
.
Hosts-level Structural Details
As you can imagine there will be some significant differences in configuration of different hosts but there will also be some overlap. For example, on my desktop I might have Audacity, the program I'm recording this audio with, but I wouldn't have a need for that on a theater host. However, I'll likely have the same browser, possibly the same windows manager, and definitely some of the same users. Where ever sensible I want these to be configured same way on all my hosts so I avoid duplicate config files while benefiting from a consistent experience across my network.
To organize this I use a common
directory and within it I have a subdirectory for each user on my network.
E.g.:./hosts/common/some-user
All of the configuration settings for how these users should be created, what their passwords and other secrets might be, and importantly what home-manager configs to reference from the home section are included in these files. Any distinct users that would need to be created on a given system would be added here.
The next common subdirectory is core
, and this includes configs are for any non-user, system-level settings that are present or required on all of the hosts in my network. This is going to include things like my localization defaults, nixos-specific configs like garbage collection, services such as ssh, sops for secrets management, and my basic shell settings. All of the settings and packages that, regardless of what system in my network I'm using, I expect to be present and the same.
The last common subdirectory is optional
, which includes all of the other system-level configs. Some of these might appear on one system and not others, some may appear on all hosts but they are not considered part of the core because you know that an additional host down the road definitely won't require it. So for example, this is where I would put a browser, a windows manager, msmtp server, pipewire audio, etc. These are all packages and settings that won't be needed on all of my hosts.
common
Shared configurations consumed by the machine specific ones.core
Configurations present across ALL hosts. This is a hard rule; if something isn't core, it is optional.optional
Optional configurations present across more than one host.users
Host level user configurations present across at least one host.
<hostname>.nix
Host-specific declarations and importation of common configs.
Home
Path: ./home/<user>
All of the user-level preferences are housed at the home level. You could think of this as where your dotfiles are declared. Home configs handle package installation and user preferences that are specific to individual users. Furthermore, each user configured here can define host specific preferences.
User-level Home-manager configurations accessible via home-manager switch --flake .#<user>@<host>
.
Home-level Structural Details
Each of the users created under ./hosts/common/users
references a ./home/<user>/<host>.nix
config file.
The commonality of settings that was true to hosts configurations is also true to home configurations. As a user, I'm likely to want the same personal preferences across most, if not all, of the systems I access. So we make use of ./home/<users>/common
directory in this substructure as well. Just like with hosts, this directory will also have a core
that is always included for the user regardless of which host they are on. This would include things like environment settings, fonts, personal git config, primary editor settings, and personal shell settings.
And likewise, we also use an 'optional' subdirectory within ./home/<user>/common
for personal configurations that do not occur on all hosts. This would be things like personal windows manager customizations like wallpaper, theme, and tile decorations would be declared as well as preferences for individual applications and services.
common
Shared home-manager configurations consumed the user's machine specific ones.core
Home-manager configurations present for user across all machines. This is a hard rule! If something isn't core, it is optional.optional
Optional home-manager configurations that can be added for specific machines. These can be added by category (e.g. options/media) or individually (e.g. options/media/foo.nix) as needed.
The home-manager core and options are defined in host-specific .nix files housed in home/<user>
.
Putting it all together
The flake.nix
file is our central staging area that pulls in all of our external dependencies.
These include URLs to official package sources, potentially unofficial sources, and other flakes.
The dependencies are used by the flake outputs, which include any custom nixos or home-manager modules we have, any overlays, or custom packages we've written, a development shell and nix formatter, all of our system-level, host configurations settings to define what users get created and how each system should be configured. And lastly all of our user-level, home-manager configuration settings to define individual user preferences across the hosts.
Even if you don't yet use all of the elements of the anatomy, such as overlays or custom modules, knowing how they are distinguished and organized will be helpful when the time comes and when referring to other public configs that follow a similar structure.
References and acknowledgements
My nix-config on GitHub.
A video version of this article with additional analogies and context.
My own nix-config is currently based heavily on Mysterio77's Personal nix-config. He also has a starter template that may be of interest.
The inspiration for creating a diagram for my nix-config came from Eric Tossel's nixflakes.
* Updated March 5, 2024 - References and acknowledgements mentioned in the video were added to the article.