Config repo layout¶
Overview¶
A config mono-repo is a single flake that holds NixOS systems, optional Home Manager profiles, shared modules, and sometimes custom packages. There is no upstream-mandated tree, but most repos converge on the same shape: flake.nix at the root, one module per host under hosts/, reusable role modules under modules/, and flake inputs threaded through specialArgs rather than read from config during import resolution.
This page describes folder conventions and how they connect to nixosConfigurations and homeConfigurations. At org scale it also covers private inputs, CI host matrices, ownership, secrets placement, registries, and when to split flakes. Output wiring and module semantics are covered in those pages and under 09-nixos.
Details¶
Typical directory layout.
| Path | Role |
|---|---|
flake.nix |
Inputs, nixosConfigurations, optional homeConfigurations, exported modules |
hosts/<hostname>/default.nix |
Per-machine entry module: imports roles + hardware-configuration.nix, host-only overrides |
modules/ |
Shared NixOS (or HM) modules — roles such as desktop.nix, server.nix, networking.nix |
users/<name>/home.nix |
Optional per-user Home Manager module when not colocated with the host |
overlays/ |
Nixpkgs overlays applied from host or shared modules |
pkgs/ |
Custom packages built with callPackage and referenced from modules |
secrets/ |
Optional encrypted secret files / key lists (never plaintext tokens) |
Filenames are conventions, not requirements. The pattern is thin entry modules that imports focused fragments; see Imports and profiles.
Repo shape (conceptual).
flowchart TB
subgraph flake["flake.nix"]
inputs["inputs (nixpkgs, home-manager, …)"]
nixosCfg["nixosConfigurations.*"]
hmCfg["homeConfigurations.* (optional)"]
exports["nixosModules / home-managerModules"]
perSys["perSystem (optional, e.g. flake-parts)"]
end
subgraph hosts["hosts/"]
laptop["laptop/default.nix"]
server["server/default.nix"]
end
subgraph shared["shared layers"]
modules["modules/ (roles)"]
overlays["overlays/"]
pkgs["pkgs/"]
users["users/*/home.nix"]
end
inputs --> nixosCfg
inputs --> hmCfg
nixosCfg --> laptop
nixosCfg --> server
laptop --> modules
server --> modules
laptop --> users
modules --> overlays
modules --> pkgs
exports --> modules
When to split: hosts, roles, or users.
| Split at | Put here | Use when |
|---|---|---|
| Host | hosts/<hostname>/default.nix |
Machine-specific facts: hostname, disks/bootloader (hardware-configuration.nix), NIC names, one-off service toggles, which roles apply on this box |
| Role | modules/<role>.nix |
Reusable capability shared by several hosts: desktop stack, NAS services, VPN, monitoring agent — anything two machines might import unchanged |
| User | users/<name>/home.nix |
Dotfiles, editor/shell config, per-user packages — owned by a login, not by hardware; may be standalone HM or embedded in a host via home-manager.users |
Rule of thumb: if removing a machine from the fleet would delete the setting, it belongs in hosts/; if adding a second machine would copy-paste the same block, promote it to modules/; if it follows the person across laptops and servers, put it under users/.
Wiring hosts in flake.nix. Each machine gets a nixosConfigurations.<host> entry. Pass flake inputs into every module via specialArgs so host and role modules can use inputs without importing the flake root:
nixosConfigurations.laptop = nixpkgs.lib.nixosSystem {
modules = [ ./hosts/laptop/default.nix ];
specialArgs = { inherit inputs; };
};
Do not try to pass inputs by reading merged config inside imports — import lists must be static (see failure modes below).
specialArgs vs _module.args. These solve different layers of the module graph:
| Mechanism | Set from | Visible in | Typical use |
|---|---|---|---|
specialArgs |
nixosSystem / homeManagerConfiguration (extraSpecialArgs) |
Every module in that configuration | Flake inputs, self, paths that must appear in imports |
_module.args |
Inside any module’s config |
Descendant modules in the same evaluation | Values computed inside the module graph (shared cfg, internal helpers) |
specialArgs is not overridable via the module system; _module.args merges like other module options. For flake inputs and static import paths, prefer specialArgs. Use _module.args only when the value must come from within the module fixpoint. See writing a module.
Host entry module. hosts/<hostname>/default.nix typically:
importsshared role modules frommodules/(and optionallyhome-manager.nixosModules.home-manager).imports./hardware-configuration.nix(generated at install; disk and bootloader specifics stay here).- Sets only what differs for this host — hostname, networking, one-off service toggles.
Role modules encode capabilities (desktop, NAS, hypervisor); the host file picks which roles apply. Multiple hosts import the same modules/server.nix but differ in hostname and hardware fragments.
Overlays and custom packages. Apply overlays from a host or shared role so every host that imports the role sees the same package set:
# modules/common.nix (or a host entry)
{ ... }: {
nixpkgs.overlays = [
(import ../overlays/default.nix)
];
}
Keep one nixpkgs input in flake.nix and align downstream inputs with inputs.<name>.follows = "nixpkgs" so flake.lock does not pull a second Nixpkgs checkout. Custom derivations under pkgs/ are usually built via pkgs.callPackage inside modules after overlays are applied.
Home Manager placement. Two common patterns:
- Standalone —
homeConfigurations.<user>inflake.nix, modules underusers/<name>/home.nix. Use on non-NixOS hosts or when dotfiles evolve on a separate cadence. See homeConfigurations. - NixOS-embedded — import
home-manager.nixosModules.home-managerin the host module list and sethome-manager.users.<user> = ./users/<name>/home.nix(or an inline module). User env rebuilds withnixos-rebuild, nothome-manager switch.
Both can coexist in one repo for different users or machines. When embedded, set home-manager.useGlobalPkgs = true and home-manager.useUserPackages = true so Home Manager reuses the system pkgs and user profile layout; without useGlobalPkgs, HM builds against its own pkgs and can drift from system packages.
Exporting reusable modules. Flakes can expose nixosModules.<name> and home-managerModules.<name> so other flakes import your roles without copying files:
outputs = { ... }: {
nixosModules.desktop = ./modules/desktop.nix;
nixosModules.server = ./modules/server.nix;
};
Reference them in the same repo (imports = [ inputs.self.nixosModules.desktop ]) or from downstream flakes. Exported modules are plain module paths or functions — same semantics as inline imports.
Deploy tools. Colmena, Morph, nixinate, and similar tools read nixosConfigurations.<name> from your flake outputs and build or activate the matching .config.system.build.toplevel. Layout under hosts/ does not change the deploy interface: the flake output name is the stable address (.#server, .#laptop).
Packages and checks (brief). Mono-repos often also define packages, apps, or checks per system. Frameworks such as flake-parts expose these via perSystem without hand-rolling eachSystem in flake.nix; the hosts/modules/users split stays the same. See that page for mkFlake and perSystem details — not duplicated here.
Framework alternatives. The same decomposition problem — many hosts, shared roles, several output keys — is solved with scaffolds that map directories to outputs:
- flake-parts — module-system evaluation of
outputs,perSystemfor packages and checks. - Snowfall, Blueprint — opinionated folder → output conventions with a thin
flake.nix.
Use a framework when manual flake.nix glue becomes noisy; the underlying layout (hosts, modules, users) stays recognizable.
Private flake inputs¶
A config mono-repo often depends on private org flakes (github:org/private-repo, git+ssh://…, private GitLab). HTTPS GitHub/GitLab fetches need credentials on the machine or CI runner — typically access-tokens in nix.conf, or a netrc file — never committed next to flake.nix. SSH URLs use the runner’s deploy keys or agent instead.
Keep private input URLs in flake.nix / flake.lock as ordinary flakerefs; auth is a client concern (developer laptop, CI secret store), not a layout concern. CI wiring for private inputs is covered under private flakes and CI.
Multi-host CI matrix¶
Org fleets usually want CI to prove that hosts still evaluate and build. Common patterns:
checks— expose named derivations underchecks.<system>.*(e.g. wrapnixosConfigurations.laptop.config.system.build.toplevel, or lighter smoke tests).nix flake checkbuilds them. See checks and hydraJobs.- Explicit matrix builds — CI jobs that
nix build .#nixosConfigurations.<host>.config.system.build.toplevelfor selected hosts (same installable Colmena/Morph/nixinate deploy against).
Building every host on every PR is often too expensive. Treat path filters and host groups as a repo/CI convention, not a Nix feature: e.g. changes under hosts/laptop/ or modules/desktop.nix rebuild the laptop group; modules/common.nix rebuilds a wider set. Document the groups in the CI config (or a small script that maps changed paths → hosts). Full runner setup and caches: CI with Nix.
Team ownership¶
Use forge CODEOWNERS (or equivalent) so reviews map to directories: hosts/<team-host>/ → host owners; modules/<domain>/ → platform or service owners; users/ → individuals. This is a Git-forge convention layered on the folder split above — Nix does not enforce it. Keep ownership lines coarse (directory roots) so CODEOWNERS stays maintainable as hosts grow.
Secrets in mono-repos¶
Never commit forge tokens, access-tokens values, age/sops private keys, or plaintext secrets beside the flake. Encrypted secret material and the NixOS modules that consume it usually live under modules/ (shared policy) and/or a dedicated secrets/ tree (encrypted files + key lists). Host entries only select which secrets apply.
See secrets strategies and agenix / sops-nix for tooling; this page only places those files in the tree.
Private registries¶
nix registry aliases (nixpkgs, org shorthand ids) are optional CLI convenience. They do not replace flake.lock pins for the config repo, and they do not supply credentials: fetching a private github:… or HTTPS git URL still needs access-tokens / netrc (or SSH) on the client. Prefer explicit input URLs in flake.nix for shared mono-repos so collaborators are not dependent on a local registry pin.
Multiple flakes vs one mono-repo¶
| Prefer | When |
|---|---|
| One flake | Shared flake.lock, shared modules/, one PR can update roles + all hosts; deploy tools address nixosConfigurations.* from a single root |
| Split flakes | Different release cadences (dotfiles vs infra), hard ACL boundaries (team A must not evaluate team B’s hosts), or separate CI/cache identities |
A common middle ground: one infra mono-repo flake for NixOS hosts, plus a smaller flake for personal Home Manager — linked via an input when needed, not forced into one lockfile. Exported nixosModules (above) let a split flake reuse roles without copying trees.
Boundaries¶
This page covers where files live and how folders connect to flake outputs. It does not define:
- Individual option trees or service modules (see 09-nixos and configuration.nix).
- Full
nixosSystem/homeManagerConfigurationAPI tables (nixosConfigurations, homeConfigurations). perSystem,mkFlake, or flake-parts module options (flake-parts).- Remote deploy flags or disk partitioning (remote deploy).
- Full secrets tooling or CI runner recipes (secrets strategies, CI with Nix).
Failure modes¶
- Conditional
importsfromconfig—importsis resolved before the module fixpoint; you cannotimporta path chosen fromconfig.services.*.enable. UsemkIfon options inside static modules, or separate host entry files per role combination. - Inputs only via
config— flake inputs belong inspecialArgs/_module.args, not reconstructed from option values after merge. - Duplicate Nixpkgs pins — forgetting
inputs.home-manager.inputs.nixpkgs.follows = "nixpkgs"(or similar) pulls a second Nixpkgs revision into the lockfile; system and HM builds may disagree on package versions. - HM / NixOS package drift — embedded Home Manager without
useGlobalPkgsevaluates against a separatepkgsthan NixOS;environment.systemPackagesandhome.packagescan install different revisions of the same name. - One giant
configuration.nix— hard to review, merge-conflict prone, and difficult to reuse across hosts. Split by role and import. - Hostname scattered in role modules —
networking.hostNamebelongs in the host entry; role modules should stay hostname-agnostic so they compose on any machine. - Committed credentials — tokens or private keys in the repo (or in
flake.nix) — use local/nix.confauth and encrypted secret stores instead. - CI rebuilds the entire fleet on every PR — without path filters or host groups, large mono-repos burn CI budget; narrow the matrix by convention.
Examples¶
Illustrative mono-repo tree:
.
├── flake.nix
├── hosts/
│ ├── laptop/
│ │ ├── default.nix
│ │ └── hardware-configuration.nix
│ └── server/
│ ├── default.nix
│ └── hardware-configuration.nix
├── modules/
│ ├── common.nix # overlays + baseline
│ ├── desktop.nix
│ └── server.nix
├── users/
│ └── alice/
│ └── home.nix
├── overlays/
│ └── default.nix
├── pkgs/
│ └── my-cli/
│ └── default.nix
└── secrets/ # encrypted only; optional
└── …
Multi-host flake.nix with shared inputs, two nixosConfigurations, exported module, and standalone HM:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, home-manager, ... }@inputs: {
nixosConfigurations.laptop = nixpkgs.lib.nixosSystem {
specialArgs = { inherit inputs; };
modules = [ ./hosts/laptop/default.nix ];
};
nixosConfigurations.server = nixpkgs.lib.nixosSystem {
specialArgs = { inherit inputs; };
modules = [ ./hosts/server/default.nix ];
};
nixosModules.desktop = ./modules/desktop.nix;
nixosModules.server = ./modules/server.nix;
homeConfigurations.alice = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
extraSpecialArgs = { inherit inputs; };
modules = [ ./users/alice/home.nix ];
};
};
}
Shared overlays via a role module:
# modules/common.nix
{ ... }: {
nixpkgs.overlays = [
(import ../overlays/default.nix)
];
environment.systemPackages = [ pkgs.my-cli ];
}
Server host — same roles pattern, different imports and hostname:
# hosts/server/default.nix
{ inputs, ... }: {
imports = [
../../modules/common.nix
../../modules/server.nix
./hardware-configuration.nix
inputs.home-manager.nixosModules.home-manager
];
networking.hostName = "server";
}
Laptop host with embedded Home Manager:
# hosts/laptop/default.nix
{ inputs, ... }: {
imports = [
../../modules/common.nix
inputs.self.nixosModules.desktop
./hardware-configuration.nix
inputs.home-manager.nixosModules.home-manager
];
networking.hostName = "laptop";
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.alice = ../../users/alice/home.nix;
}
Deploy: sudo nixos-rebuild switch --flake .#laptop or .#server. Standalone HM for the same user: home-manager switch --flake .#alice.
References¶
- nix.dev — Flakes tutorial — inputs, outputs, and
nixosConfigurationsbasics - flake.parts — module-system flake outputs
- Home Manager manual — Nix Flakes — standalone and NixOS-module integration (experimental)
See also¶
- Inputs and outputs — conventional flake output keys
- nixosConfigurations —
nixosSystemwiring and rebuild - homeConfigurations — standalone Home Manager flakes
- checks and hydraJobs — CI-oriented flake outputs
- Registries and refs — flake registry vs locked inputs
- Access tokens — auth for private HTTPS flake inputs
- Private flakes and CI — private inputs on CI runners
- CI with Nix — forge runners and caches
- Imports and profiles — static
importsand splitting configuration - configuration.nix — primary machine configuration file
- Secrets strategies — how secrets enter the module graph
- Multi-host config repo — worked fleet flake walkthrough
- agenix / sops-nix — encrypted secret modules
- flake-parts —
mkFlakeandperSystem - Dotfiles patterns — organizing user-level modules