Home Manager on NixOS
Home Manager on NixOS
Goal: Wire Home Manager as a NixOS module (not standalone-first), manage one user’s shell/git/ssh client config, and rebuild with a single nixos-rebuild switch --flake activation story.
House default for this book: Home Manager NixOS module on NixOS hosts. Standalone HM is for non-NixOS machines or special cases—know it exists; do not make it your lab’s primary path.
Why this chapter exists
Host modules put packages on the system (environment.systemPackages). That is correct for daemons and multi-user tools—and wrong as the only place for personal developer config. Home Manager (HM) owns:
- User programs (
programs.git,programs.bash/zsh/fish, …) - Dotfiles and XDG (next chapter)
- User systemd services (HM services chapter)
Doing HM as a NixOS module means: one flake, one rebuild, one generation story for system + user.
Theory 1 — Three ways people run Home Manager
| Mode | How it activates | When to use |
|---|---|---|
| NixOS module | Part of nixos-rebuild |
Default on NixOS (this book) |
| Standalone | home-manager switch --flake ... |
Other distros; or isolated user experiments |
| nix-darwin module | macOS analog | Out of scope here |
Why module mode first
- Aligns HM with system generations (easier mental model for labs)
users.users.<name>andhome-manager.users.<name>stay in one repo- Avoids “system switched but HM forgotten” dual-track ops
Standalone still matters if you develop on Ubuntu with HM-only—but your NixOS lab should not require two tools for every change.
Theory 2 — Input wiring (26.05-aligned)
# flake.nix (merge with your layout)
{
description = "NixOS lab with Home Manager";
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 = { self, nixpkgs, home-manager, ... }@inputs: {
nixosConfigurations.lab = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./hosts/lab
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# home-manager.extraSpecialArgs = { inherit inputs; };
home-manager.users.alice = import ./homes/alice;
}
];
};
};
}| Option | Meaning |
|---|---|
useGlobalPkgs = true |
HM uses the same pkgs as the system (consistent overlays later) |
useUserPackages = true |
User packages install into user profile via HM |
home-manager.users.alice = ... |
That user’s HM module tree |
extraSpecialArgs |
Pass flake inputs into HM modules when needed |
Version match: HM release-26.05 with nixpkgs nixos-26.05. Mismatched releases are a common source of obscure option errors.
Where to put the HM block
| Style | Pros |
|---|---|
Inline in flake.nix as above |
Obvious for one host |
Dedicated module modules/common/home-manager.nix |
Cleaner multi-host |
Per-host in hosts/lab/default.nix |
Different users per machine |
All are valid; pick one and document it.
Theory 3 — System user vs HM user
You still need a NixOS user:
# modules/common/users.nix
{
users.users.alice = {
isNormalUser = true;
extraGroups = [ "wheel" ];
# shell can be set here OR via HM programs.* — pick one source of truth
};
}HM configures that user’s home:
# homes/alice/default.nix
{ pkgs, ... }:
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "26.05"; # set once; do not casually bump
programs.home-manager.enable = true;
programs.git = {
enable = true;
userName = "Alice Example";
userEmail = "alice@example.invalid";
};
programs.bash = {
enable = true;
enableCompletion = true;
};
home.packages = with pkgs; [
ripgrep
fd
];
}home.stateVersion
Like NixOS system.stateVersion, this is a compatibility pin for HM stateful defaults—not “current release marketing.” Set it to the HM release you first adopt for that user; read release notes before bumping.
Name and path must match
| Field | Must agree with |
|---|---|
home-manager.users.alice |
attr name |
home.username |
"alice" |
home.homeDirectory |
actual home, usually /home/alice |
users.users.alice |
system user exists |
Mismatches produce activation failures or configs landing in the wrong place.
Theory 4 — What belongs in HM vs NixOS
| Concern | Prefer |
|---|---|
sshd, firewall, kernel, system users |
NixOS modules |
| Interactive shell, prompt, git, editor config | HM |
| CLI tools only you use | HM home.packages |
| Libraries/services shared by system units | NixOS |
| Secrets for user apps | Secrets chapters (not plaintext in HM) |
| Network daemons (nginx, postgres) | NixOS |
| Project compilers | Project devShells + direnv (later chapter) |
Anti-pattern: dumping everything into environment.systemPackages because HM felt optional. Opposite anti-pattern: putting nginx only in HM.
Theory 5 — Activation model (what rebuild does)
On nixos-rebuild switch with HM module:
- System config builds
- HM config for each defined user builds
- Activation scripts run: system switch + HM activation for users
Failures can be system or HM. Read the traceback: home-manager-alice.service style units often appear in logs.
journalctl -u home-manager-alice.service -b --no-pager | tail -80(Username substitution as appropriate.)
Standalone commands (awareness)
Even in module mode you may see:
home-manager generations 2>/dev/null || true
ls -la ~/.local/state/nix/profiles 2>/dev/null || trueYou should not need home-manager switch for normal changes if module mode is correct.
Theory 6 — Programs modules vs raw packages
HM programs.* modules often:
- Install the package
- Generate config files
- Wire shell integration
Example: programs.git.enable = true vs only home.packages = [ pkgs.git ] (no managed config). Prefer programs modules when they exist for tools you configure.
Discover options:
- Home Manager option search
- Or eval/docs from your locked HM input
Shell source of truth
| Approach | Note |
|---|---|
users.users.alice.shell = pkgs.bashInteractive |
System login shell |
programs.bash.enable = true |
HM manages bash config |
| Both fighting | Pick login shell once; let HM own rc files |
Theory 7 — Splitting the home tree
homes/alice/
default.nix # imports + identity (username, stateVersion)
programs.nix # git, bash, ssh client
files.nix # later: xdg / home.file
services.nix # later: user systemd
# homes/alice/default.nix
{
imports = [
./programs.nix
# ./files.nix
# ./services.nix
];
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "26.05";
programs.home-manager.enable = true;
}Theory 8 — Backup and collision with existing dots
HM activation may refuse to overwrite an existing regular file with a symlink. Before first activation of a path:
ls -la ~/.bashrc ~/.config/git 2>/dev/null || true
# backup and move aside if needed| Situation | Approach |
|---|---|
| Imperative bashrc | Backup; let HM own it |
| Mixed partial HM | Finish migration; avoid dual management |
| One-off experiment file | Keep outside HM paths |
Worked example — Minimal useful user
# homes/alice/default.nix
{ pkgs, lib, ... }:
{
home.username = "alice";
home.homeDirectory = "/home/alice";
home.stateVersion = "26.05";
programs.home-manager.enable = true;
programs.git = {
enable = true;
userName = "Alice";
userEmail = "alice@example.invalid";
extraConfig = {
init.defaultBranch = "main";
pull.rebase = true;
};
};
programs.ssh = {
enable = true;
# Do not put private keys in the flake. Config only:
matchBlocks = {
"lab-remote" = {
hostname = "192.0.2.10"; # documentation range example
user = "alice";
};
};
};
programs.bash = {
enable = true;
shellAliases = {
ll = "ls -la";
".." = "cd ..";
};
};
home.packages = with pkgs; [ htop ];
}Private keys stay in ~/.ssh/ with correct permissions—never in git, never as world-readable store strings (secrets chapters).
Worked example — HM wiring module (multi-host friendly)
# modules/common/home-manager.nix
{ inputs, ... }:
{
imports = [
inputs.home-manager.nixosModules.home-manager
];
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.alice = import ../../homes/alice;
};
}Requires specialArgs = { inherit inputs; }; on nixosSystem (or equivalent) so inputs is available.
Exercises
Exercise 1 — Add the input and lock
cd /path/to/your-flake
# edit flake.nix inputs + outputs as in Theory 2
nix flake lock
# or: nix flake update home-manager
git diff flake.lock | head -80Confirm follows for nixpkgs.
Exercise 2 — Stub → real home module
mkdir -p homes/aliceWrite homes/alice/default.nix with git + bash + one package. Wire HM NixOS module and home-manager.users.<you>.
Ensure users.users.<you> exists on the system side with matching name and home directory.
Exercise 3 — Rebuild and verify
sudo nixos-rebuild switch --flake .#labAs the target user:
git config --global --list | head
which rg || which htop
echo "$HOME"
home-manager generations 2>/dev/null || trueExercise 4 — Change only HM, rebuild once
Edit programs.git.userName, rebuild, confirm new value. You should not need a separate home-manager switch if module mode is correct.
Exercise 5 — Deliberate failure drill
programs.git.enabl = true; # intentional typoRebuild, read the error, fix. Note whether the failure is clearly HM evaluation.
Exercise 6 — Journal: boundary decisions
List 10 tools you use daily. Mark each NixOS / HM / either / project shell. Resolve at least the ambiguous ones with a one-line reason.
Exercise 7 — Activation log literacy
journalctl -u home-manager-$USER.service -b --no-pager | tail -100Save one successful and (from Exercise 5) one failed snippet in notes (redact secrets).
Exercise 8 — Split programs.nix
Move git/bash/ssh into homes/alice/programs.nix and import it. Rebuild; behavior unchanged.
Exercise 9 — Package placement check
Add a tool to home.packages, rebuild, verify command -v. Confirm it is not required for other users of the host.
Exercise 10 — Shell source of truth
Document in docs/layout.md (or home notes): where login shell is set, where aliases live.
Exercise 11 — Backup existing dots
Before enabling a programs.* that manages a file you already have, backup:
cp -a ~/.gitconfig /tmp/gitconfig.bak 2>/dev/null || trueExercise 12 — Version alignment proof
nix flake metadata | tee /tmp/flake-meta.txtConfirm HM input tracks release-26.05 and nixpkgs nixos-26.05.
Exercise 13 — Negative case: standalone not required
Write two sentences: when you would use standalone HM; why the lab does not.
Common gotchas
| Symptom / mistake | What to do |
|---|---|
| HM release ≠ nixpkgs release | Align release-26.05 with nixos-26.05 |
| User name mismatch | users.users.x must match home-manager.users.x |
home.homeDirectory wrong |
Must match actual home path |
Infinite recursion with useGlobalPkgs |
Check for pkgs/HM cycles; simplify modules |
| Expecting standalone CLI only | Module mode activates via nixos-rebuild |
| Private keys in flake | Remove immediately; rotate if committed |
Bumping home.stateVersion casually |
Read HM release notes first |
| Shell set in three places | One source of truth (NixOS or HM for login shell) |
| Activation blocked by existing file | Backup/remove blocker |
Forgot follows |
Two nixpkgs; fix and re-lock |
| HM user without system user | Create users.users.* first |
| Putting sshd in HM | System service → NixOS module |
Checkpoint
- HM input pinned with
followsnixpkgs - HM NixOS module imported in flake
- At least one user has git + shell via HM
- Single
nixos-rebuild switch --flakeactivates system + HM - No private key material in the repo
home.stateVersionset consciously- Boundary list (NixOS vs HM vs project) started
- Know where to read HM activation failures
Journal (optional)
Write three personal gotchas before continuing.