A simple Nix dendritic config
I run NixOS on both my desktop and laptop. Until a few weeks ago, all my Nix code lived in
a single configuration.nix file that accepted the machine hostname as an argument. Over
time, entropy did its work, and that file turned into a spaghetti mess.
After seeing the Nix Dendritic Pattern mentioned in several places, I wondered whether it would make sense for my rather simple setup, or if it would be overkill. Spoiler: even for my simple setup, it helped me better organize my system configuration.
There are great guides, posts, and examples in the wild to learn about the dendritic pattern and its components. Nevertheless, I had a hard time applying the abstract concepts to my concrete problem. I often had to dive into implementation code to fully understand some parts.
In this post, I explain how I used the dendritic pattern to solve my particular problem: NixOS + Home Manager on both a desktop and a laptop, with two users. I trimmed my configuration to the minimum while keeping all features. The goal is a complete but simple example that we can walk through and explain in full. I hope that after grasping this example, you should be able to create a solution tailored to your own requirements with the help of other sources.
I assume familiarity with Nix, NixOS, and flakes.
All the code implementing the systems configuration is available here:
/etc/nixos ├── flake.nix └── modules ├── base │ ├── configuration.nix │ ├── overlays.nix │ └── packages.nix ├── flake-parts.nix ├── hosts │ ├── home-desktop │ │ ├── default.nix │ │ └── filesystem.nix │ └── renato-notebook │ ├── default.nix │ └── filesystem.nix └── users ├── helena.nix └── renatofg ├── default.nix └── _dotfiles └── starship.toml
We’ll cover the key concepts first, then walk through the code.
Contents
Key concepts
In short: we organize our configuration as a collection of modules that get merged together.
But to understand how this works, we first need to cover a few core concepts, the building blocks we use to structure our system configuration following the dendritic pattern.
Flake
We use all those files to structure our NixOS system configuration. At the
end of the day, all that code gets evaluated into a flake output
set. Specifically, the system configuration
lives under the nixosConfigurations.<hostname> attribute.
We write our configuration as Flake Parts modules spread across
multiple .nix files following the dendritic pattern. These files are imported and passed
to the mkFlake function, which evaluates them into the final flake outputs.
Module
The module is the fundamental unit upon which everything else is built. We break our configuration into “module files,” where each file configures a specific feature. How broad or narrow each feature is, and how many you create, is up to you. We’ll discuss this further in the following sections.
At its core, a module is a function receiving an attribute set as input and evaluating
to another attribute set. Optionally, instead of being a function, a module can be just
a plain attribute set (like Module C). In this case, a module { A } will
behave the same as the constant function args: { A }.
Once we split our configuration into multiple modules, we can run nixos-rebuild to
reconfigure our NixOS machine. The module system combines all those modules into one. When
evaluated, it produces the final merged attribute set.
When merging modules, nested sets merge recursively (attribute by attribute), and lists are concatenated. However, if two modules define the same scalar value, it raises an error.
If we have these three modules:
{ pkgs, ... }:
{
networking.wireless.enable = true;
environment.systemPackages = [
pkgs.nixd
pkgs.emacs
pkgs.git
];
}Nix
{ pkgs, ... }:
{
networking.wireless = {
dbusControlled = true;
fallbackToWPA2 = true;
};
environment.systemPackages = [
pkgs.pandoc
];
}Nix
{
networking.hostName = "home-desktop";
}Nix
After merging them, the result will be:
{ pkgs, ... }:
{
networking = {
wireless = {
enable = true;
dbusControlled = true;
fallbackToWPA2 = true;
};
hostName = "home-desktop";
};
environment.systemPackages = [
pkgs.nixd
pkgs.emacs
pkgs.git
pkgs.pandoc
];
}Nix
Everything discussed so far applies to all modules. However, there are different types of modules, distinguished by their input and output sets. Below we cover the three module types we’ll use: NixOS modules, Home Manager modules, and Flake Parts modules.
NixOS modules
NixOS modules configure the system. They are
functions that receive an attribute set { config, options, pkgs, lib, modulesPath, ... }
(the official wiki describes each
argument) and evaluates to a set with NixOS options like services.*, users.*,
networking.*, boot.*, etc. (See Appendix
A for a full list.)
Here’s an example:
{ pkgs, ... }:
{
environment.systemPackages = [
pkgs.nixd
pkgs.git
pkgs.emacs-with-packages
];
services.openssh.enable = true;
users.users.renatofg = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" ];
};
}Nix
This module sets three options: environment.systemPackages (system-wide packages),
services.openssh.enable (enable SSH daemon), and users.users.renatofg (user account
configuration).
A classic example of a NixOS module is /etc/nixos/configuration.nix, created during a
fresh install.
Home Manager modules
Home Manager
modules
configure the user environment (dotfiles, programs, user services). Like NixOS modules,
they are functions that receive an attribute set { pkgs, lib, config, options, modulesPath, osConfig, ... } (see the official
documentation
for details) and evaluates to a set with options like home.*, programs.*, services.*
(user-level), xdg.*, etc. (See Appendix
A for a full list.)
Here’s an example:
{ pkgs, osConfig, ... }:
{
home.stateVersion = "24.05";
programs.git = {
enable = true;
userName = "Renato Garcia";
userEmail = "foo@example.com";
};
programs.starship = {
enable = true;
settings = builtins.fromTOML (builtins.readFile ./_dotfiles/starship.toml);
};
# Access NixOS config when integrated
home.packages = lib.optionals osConfig.services.xserver.enable [
pkgs.alacritty
];
}Nix
Home Manager has its own module evaluation. It can run
standalone or
integrated with
NixOS.
When integrated via
home-manager.users.<name>,
each user’s configuration is evaluated separately from the system configuration. The
osConfig argument provides read-only access to the NixOS configuration, letting you make
Home Manager settings conditional on system settings.
Flake Parts modules
Flake Parts modules differ from NixOS and Home Manager modules. Instead of configuring systems or users, they structure the flake itself, using the module system to organize flake outputs.
Like other modules, they are functions that receive an attribute set { self, config, options, lib, inputs, ... } and evaluate to another attribute set. (See the documentation
for input arguments and output
options.)
The Flake Parts library has broad applications, but for our purposes we only need three
options:
flake.modules.<class>.<aspect>,
flake.nixosConfigurations.<hostname>,
and
perSystem.overlayAttrs.
flake.modules.<class>.<aspect>
This option collects our NixOS modules and Home Manager
modules. The <class> attribute designates the module type,
either nixos or homeManager (other classes exist but we won’t use them here). The
<aspect> attribute designates the feature being configured.
For example: common configuration shared by all hosts goes in the base aspect
(example),
user-specific configuration in the helena aspect
(example), and a
complete host configuration in the home-desktop aspect
(example).
You could also create an aspect for a specific service like ssh:
{ inputs, config, ... }:
{
flake.modules.nixos.ssh = {
# A NixOS module: setup OpenSSH server, firewall-ports, etc.
};
flake.modules.homeManager.ssh = {
# A Home Manager module: setup ~/.ssh/config, authorized_keys,
# private keys secrets, etc.
};
}Nix
The <class> name must match a configuration class: nixos, homeManager, etc. This
determines what module type to assign. For a nixos class, assign NixOS
modules; for homeManager, assign Home Manager
modules.
The <aspect> name is arbitrary, use as many as you need to organize your configuration.
As with all modules, you can define multiple flake.modules.nixos.base
attributes and they’ll be merged together. Access the merged result from any Flake Parts
module via self.modules.nixos.base.
flake.nixosConfigurations.<hostname>
This option maps to the flake’s
outputs.nixosConfigurations.<hostname>
attribute. The <hostname> matches what you specify in nixos-rebuild switch --flake /etc/nixos#<hostname>.
Code walkthrough
Now let’s walk through the code.
flake.nix
Remember: the flake output set is our
final result. When you run nixos-rebuild switch --flake /etc/nixos#home-desktop, it
evaluates /etc/nixos/flake.nix, reads the configuration from
outputs.nixosConfigurations.home-desktop, and reconfigures the system accordingly.
The function flake-parts.lib.mkFlake processes all our modules to create the host
configurations:
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } (inputs.import-tree ./modules);Nix
Instead of manually importing each Flake Parts module, we use
import-tree to recursively discover
and import all .nix files under ./modules.
Flake Parts modules
All Nix files under modules/ directory are Flake Parts modules. They
receive the arguments described in the Flake Parts section and
output a set with one or more of the following attributes:
flake.modules.<class>.<aspect>
Here nixos is the class and base is the aspect. The directory tree
follows a convention: either the file is named after the aspect (e.g., helena.nix) or
the parent directory matches the aspect name. This is not a strict requirement, just a
convenient way to organize the code.
flake.nixosConfigurations.<hostname>
Here renato-notebook is the hostname. The nixosSystem function is covered in
Defining a host.
perSystem.overlayAttrs
Here we define the overlay applied to nixpkgs. For example, we can add an attribute pointing to a different nixpkgs version:
Or override a specific package:
polybar = pkgs.polybar.override {
alsaSupport = false;
mpdSupport = true;
pulseSupport = true;
iwSupport = true;
nlSupport = true;
i3Support = false;
};Nix
Or create new packages:
emacs-with-packages = pkgs.emacs.pkgs.withPackages (
epkgs:
(with epkgs.melpaPackages; [
doom-modeline
evil-collection
magit
zenburn-theme
])
++ [
epkgs.nix-ts-mode
epkgs.tree-sitter-langs
epkgs.treesit-grammars.with-all-grammars
]
);Nix
These changes are visible when packages are installed on the system.
imports
imports = [
inputs.flake-parts.flakeModules.modules
inputs.flake-parts.flakeModules.easyOverlay
];Nix
Here we import the Flake Parts modules that provide the features used in our configuration.
inputs.flake-parts.flakeModules.modules
enables defining multiple modules as attributes in the flake.modules.<class>.<aspect>
format. Without it, we would get a “flake.modules is defined multiple times while it’s
expected to be unique” error.
inputs.flake-parts.flakeModules.easyOverlay
enables defining overlays using the perSystem.overlayAttrs
format. Without it, we would also get an error.
systems
This just needs to exist, otherwise we would get a “The option systems was accessed but
has no value defined” error. It becomes relevant when using other Flake Parts
features, but is not used in
our current configuration.
NixOS modules
All values assigned to attributes in the flake.modules.nixos.<aspect> format (i.e., all
aspects with nixos as their class) are NixOS modules. See the code here:
1,
2,
3,
4,
5,
6,
7,
8.
Within a NixOS module we can set system options, install packages, and so on. However, a
special category in our dendritic configuration are modules where the aspect matches the
host name, following the format flake.modules.nixos.<hostname>. Our example has both
flake.modules.nixos.renato-notebook
(1,
2)
and flake.modules.nixos.home-desktop
(1,
2).
These are special because they gather all the modules used by each host, specifically in
the imports attribute:
we list all the aspects used by that host. Here we included
the merged flake.modules.nixos.base modules
(1,
2), the
flake.modules.nixos.renatofg module
(1),
and flake.modules.nixos.helena
(1).
Note that when listing packages to install:
environment.systemPackages = with pkgs; [
unstable.nushell
# Language servers
bash-language-server
clang-tools
cmake-language-server
nixd
alacritty
emacs-with-packages
git
pandoc
polybar
qutebrowser
starship
];Nix
any changes made with the overlay are visible.
Home Manager modules
All values assigned to attributes in the flake.modules.homeManager.<aspect> format
(i.e., all aspects with homeManager as their class) are Home Manager modules. See
the code
here.
Defining a host
To generate the configuration for a host, we call the
nixpkgs.lib.nixosSystem
function. This is where we combine all module pieces to build a complete NixOS system
configuration. The nixosSystem function accepts several arguments, but we only use the
modules list:
flake.nixosConfigurations.renato-notebook = inputs.nixpkgs.lib.nixosSystem {
modules = [
self.modules.nixos.renato-notebook
inputs.home-manager.nixosModules.home-manager
{
home-manager = {
useGlobalPkgs = true;
users.renatofg = self.modules.homeManager.renatofg;
};
}
{
nixpkgs = {
config.allowUnfree = true;
overlays = [ self.overlays.default ];
};
}
];
};Nix
Each element in the modules list is a NixOS Module. When merged
together, they fully define the host.
The first item:
is the merged result of all flake.modules.nixos.renato-notebook modules—the NixOS
module containing the complete system configuration for the
renato-notebook host.
The second item:
is a module defined by the Home Manager
project
that enables including Home Manager configuration as part of the NixOS configuration.
This allows rebuilding both together when running nixos-rebuild switch --flake /etc/nixos#renato-notebook. Without this module, assigning options to home-manager
attributes would fail since they are not valid NixOS module options.
With home-manager.nixosModules.home-manager included, we can add the following NixOS
module to the list:
{
home-manager = {
useGlobalPkgs = true;
users.renatofg = self.modules.homeManager.renatofg;
};
}Nix
All valid options are documented
here, but the key one
is
users.<username>.
Here we add the Home Manager modules for all users on that host.
In this example, we reference our defined
flake.modules.homeManager.renatofg.
The last module in the list:
{
nixpkgs = {
config.allowUnfree = true;
overlays = [ self.overlays.default ];
};
}Nix
configures nixpkgs for our NixOS
system. All valid nixpkgs.* options are in the NixOS manual; the relevant ones here are
nixpkgs.overlays
and
nixpkgs.config.
The nixpkgs.overlays list specifies
overlays to apply to nixpkgs. In
our configuration, we apply only self.overlays.default, defined via the flake-parts
perSystem.overlayAttrs option
here.
The nixpkgs.config attribute set configures the Nixpkgs collection. Above, we only
enable unfree packages, but the same options can enable CUDA support system-wide, as done
for the home-desktop host
here.
Wrapping it up: Dendritic Pattern
dendrite /ˈden-ˌdrīt/
1. a branching treelike figure produced on or in a mineral by a foreign mineral
2. a crystallized arborescent form
3. any of the usually branching protoplasmic processes that conduct impulses toward the body of a neuron
A dendritic pattern describes a structure that branches like a tree: a trunk splits into limbs, limbs into branches, and branches into twigs.
Applied to Nix configuration, the dendritic pattern is an organizational philosophy, not a library or a set of rigid, well-defined rules. A dendritic configuration organizes code as a tree of modules:
- Base configuration shared by all systems (common packages, locale, timezone)
- Host specific configuration (hardware, hostname, host specific services)
- User specific configuration (dotfiles, per user packages, Home Manager settings)
These modules subdivide when they grow too large or complex, and merge together when evaluating a host’s configuration.
This principle extends to the directory and file structure: broad concepts like hosts
and users become directories, with modules further split into files and subdirectories
as needed.
Each module is a self-contained unit responsible for a single aspect. Modules can import other modules, creating the branching structure. A host configuration is built by combining the relevant modules.
The key dendritic insight is separation of concerns: instead of one monolithic
configuration.nix, we split configuration by what it configures (networking, users,
packages) and where it applies (all hosts, specific host, specific user). This makes it
easier to:
- Share common configuration across multiple machines
- Understand what each file does at a glance
- Add or remove features without touching unrelated code
- Reuse user configurations across different hosts
The dendritic configuration in this post is one concrete implementation among many possible variations. It is intentionally short and simple, to convey the core idea without needless complexity. However, there is no official or standardized format; the community is actively exploring alternatives and building tools for the dendritic pattern. Good references include the NixOS Discourse post where the pattern was first discussed, Vic’s Dendritic Libs for developing dendritic configurations, Doc-Steve’s guides, and the Dendrix repository for sharing dendritic configs.