Lesson 03 — NixOS vs Home Manager¶
The Split¶
Both NixOS and Home Manager configure software declaratively with Nix, but they operate at different levels:
| NixOS | Home Manager | |
|---|---|---|
| Runs as | root / system | your user |
| Config lives in | /etc/nixos/ (or in your flake) |
~/.config/home-manager/ (or in your flake) |
| Manages | system services, kernel, users, firewall, boot | dotfiles, user packages, user services |
| Applied with | sudo nixos-rebuild switch |
home-manager switch |
| Config file | configuration.nix |
home.nix |
The Rule of Thumb¶
NixOS → anything that needs root, affects all users, or is a system service.
Home Manager → anything that is per-user: shell config, editor, terminal, user packages.
Concrete Examples¶
This belongs in NixOS (system-level):¶
# Enable the Docker daemon (needs a socket, runs as root)
virtualisation.docker.enable = true;
# Add a user
users.users.marcus = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" ];
};
# Firewall
networking.firewall.enable = true;
# System-wide packages (available to all users)
environment.systemPackages = with pkgs; [ git curl wget ];
This belongs in Home Manager (user-level):¶
# Git config for MY user
programs.git = {
enable = true;
userName = "Marcus";
userEmail = "marcus@example.com";
};
# My shell
programs.fish.enable = true;
# My packages (installed in my profile, not system-wide)
home.packages = with pkgs; [ ripgrep fd bat eza ];
# A dotfile managed declaratively
home.file.".tmux.conf".source = ./tmux.conf;
How Ian Splits It¶
Ian's config (vendor/dotfiles-main/) follows this cleanly:
nixos/configuration.nix— system: hostname, timezone, fonts, docker, bluetooth, users, boothome-manager/home.nix— imports all thehome-manager/*.nixmoduleshome-manager/git.nix— git confighome-manager/ghostty.nix— terminal emulator confighome-manager/nixvim.nix— neovim (via nixvim module)home-manager/packages.nix— user packageshome-manager/sh.nix— shell (fish + starship)- etc.
Each concern has its own file. They're all imported by home.nix. This is the standard modular pattern.
Ian runs them standalone — nixos-rebuild switch and home-manager switch are separate commands.
How Wimpy Splits It¶
Wimpy's config (vendor/nix-config-main/) uses a _mixins pattern — small composable modules grouped by concern rather than by level:
nixos/_mixins/
desktop/hyprland.nix
desktop/sway.nix
services/tailscale.nix
users/martin.nix
...
home/_mixins/
apps/firefox.nix
apps/obs.nix
console/fish.nix
...
The split is the same (system vs user), but the directory names make the purpose obvious. Hosts pick which mixins they need. Wimpy also runs standalone but uses just (a task runner) to wrap the commands: just home and just host.
How Your Config (nixproject) Splits It¶
~/nixproject/ uses the same NixOS / Home Manager split but with a key difference: Home Manager runs as a NixOS module, not standalone.
nixproject/
├── modules/nixos/ ← SYSTEM layer (root, both machines)
│ ├── common.nix users, networking, Tailscale, Nix settings, binary caches
│ └── desktop.nix SDDM, Sway, KDE Plasma, PipeWire, keyring, fonts
├── modules/home/ ← USER layer (marcus, both machines)
│ ├── default.nix entry point — imports all the files below
│ ├── cli.nix bat, eza, ripgrep, fd, zoxide, atuin, git
│ ├── shell.nix zsh, aliases, starship prompt
│ ├── sway.nix Sway WM keybinds and layout
│ ├── hyprland.nix Hyprland (only activates on the EliteBook)
│ ├── terminal.nix foot, tmux
│ ├── theming.nix Catppuccin Mocha everywhere
│ ├── apps.nix Firefox, Joplin, Zoom, Jellyfin…
│ ├── selfhosted.nix Nextcloud, Bitwarden, Nheko, Thunderbird
│ ├── streaming.nix Mixxx, Audacity, OBS, EasyEffects
│ ├── editors.nix neovim, helix
│ └── webapps.nix PWA shortcuts (Grafana, Linkwarden…)
└── hosts/
├── elitebook/ machine-specific: AMD microcode, Hyprland, fingerprint reader
└── macair/ machine-specific: kernel pin, Broadcom Wi-Fi, FaceTime cam
The wiring happens in flake.nix via mkHost. Home Manager is imported as a NixOS module there, so the user layer and system layer build together:
mkHost = host: extraModules: nixpkgs.lib.nixosSystem {
modules = [
./hosts/${host}
./modules/nixos/common.nix
./modules/nixos/desktop.nix
home-manager.nixosModules.home-manager # ← HM plugged in here
{
home-manager.users.marcus = import ./modules/home;
}
] ++ extraModules;
};
One command rebuilds everything:
That applies the system config AND the Home Manager user environment in one shot. No separate home-manager switch needed.
NixOS Modules¶
A NixOS module is a .nix file that returns an attribute set with a specific schema:
{ config, pkgs, lib, ... }: # receive the standard args
{
# Declare options this module provides (optional)
options.myModule.enable = lib.mkEnableOption "my module";
# The actual configuration (conditional on the option)
config = lib.mkIf config.myModule.enable {
environment.systemPackages = [ pkgs.something ];
};
}
In practice, simple modules often skip the options section and just declare config directly:
{ pkgs, ... }:
{
environment.systemPackages = with pkgs; [ git neovim ];
services.openssh.enable = true;
}
Home Manager Modules¶
Same idea, different option namespace. Home Manager provides options under programs.*, services.*, home.*:
{ pkgs, ... }:
{
programs.starship = {
enable = true;
settings = {
format = "$all";
};
};
home.packages = with pkgs; [ ripgrep ];
home.stateVersion = "25.05"; # important — set once, don't change
}
home.stateVersion — Don't Touch This¶
This is a common source of confusion. home.stateVersion (and system.stateVersion in NixOS) records the version you first installed with. It tells NixOS/Home Manager what migration steps to skip.
Set it once when you install. Never update it. Updating it doesn't give you new features — it just breaks migrations.
Running Both Together¶
When using a flake with both NixOS and Home Manager, you have two choices:
| Approach | Command(s) | Who uses it |
|---|---|---|
| Standalone HM | sudo nixos-rebuild switch then home-manager switch |
Ian, Wimpy |
| HM as NixOS module | sudo nixos-rebuild switch (does both) |
Your nixproject |
The standalone approach lets you iterate on your user config faster without touching the system. The module approach is simpler — one command, one rebuild, always in sync.