You don't need to master Nix. You need enough working knowledge to migrate, keep the system healthy, and add things when you need to. That's it.
Go in order. Each phase builds on the one before. Stop reading when you understand the phase's goal — don't rabbit-hole.
Goal: Understand why NixOS is different from Fedora, and what the words "declarative", "immutable", "generation", and "flake" actually mean.
If you only read one thing: Zero to Nix — Concepts by Determinate Systems. It's the shortest, clearest intro. Read:
The three ideas you must hold in your head before continuing:
- Your system is a function of a config file. You edit a
.nixfile, runnixos-rebuild switch, and NixOS makes your machine match the file. You don'tdnf installthings imperatively anymore — you add them to the file. - Every rebuild is a new "generation". Old generations stick around on disk. If a rebuild breaks your system you can reboot and pick the old one from the boot menu. This is your safety net.
- Packages live in
/nix/store, not/usr/bin. Each package has its own isolated path including a hash. This is why two versions can coexist without conflict and whyFHSassumptions (bare./configure && make install, random downloaded binaries) often don't "just work".
Goal: Be able to read a configuration.nix and know roughly what it does. You are
not trying to write Nix from scratch.
Read: nix.dev — Nix language basics. Skim for an hour. Keep open as a reference while you work.
What you actually need to recognise:
{ foo = "bar"; }is an attribute set (like a dict/object)[ "a" "b" "c" ]is a list (space-separated, no commas){ pkgs, ... }: ...is a function that takes an arg set and returns somethingimport ./other-file.nixbrings another file inwith pkgs; [ neovim git ]is a shortcut that saves typingpkgs.everywhere
Don't try to memorise the semantics of let ... in, overlays, or overrides yet. They'll
click later when you actually need them.
The CHECKLIST tells you to enable flakes. Here's the "why" behind that, because it's not obvious on day one.
NixOS has two ways to describe a system:
- The classic way —
/etc/nixos/configuration.nix+ thenix-channelcommand. Packages come from a "channel" (a moving pointer to a version of nixpkgs). When you runnix-channel --update, the pointer moves. There is no file in your config that records which exact commit of nixpkgs you were on when things last worked. - Flakes — your config is a
flake.nixthat lists its inputs (nixpkgs, home-manager, etc.) by URL, and aflake.lockfile automatically pins each input to an exact git commit. You commit both files. Anyone (including future-you on a fresh install) who checks out the repo and runsnixos-rebuildgets the exact same system.
- Reproducibility, by default. The
flake.lockis the version-pin mechanism missing from the classic approach. The NixOS Wiki — Flakes page and the NixOS & Flakes Book — Enabling Flakes both lead with this as the headline benefit. Without a lock file there is no guarantee that rebuilding tomorrow gives you the same system you had today. - One standard structure. Any flake has
inputs(what it depends on) andoutputs(what it provides: a NixOS system, a dev shell, a package…). Once you've read one flake you can read all of them. Nix-without-flakes has no single structure — every project invented its own layout withdefault.nix,shell.nix,overlays/, etc. - Git-native sharing. Inputs are URLs —
github:nixos/nixpkgs,github:nix-community/home-manager. Community modules you want to pull in are just one line. This is why every modern tutorial and template (like nix-starter-configs) is flakes-first.
Flakes are still officially "experimental" — that's why you have to opt in with
nix.settings.experimental-features = [ "nix-command" "flakes" ];. They've been
experimental since Nix 2.4 (2021) and are overwhelmingly what the community uses, but
the label is real: breaking changes to the flake spec are technically possible. In
practice this has not been a problem, and the NixOS Discourse — Unofficial NixOS &
Flakes book announcement
and most community guides now assume flakes as the default starting point.
Bottom line: for a new NixOS install in 2026, going flakes-first costs you one config line of friction and buys you a version-pinned, shareable, standardised system. There's no good reason not to.
Goal: Understand how a flake-based NixOS config is laid out and how you rebuild.
Read, in order:
- NixOS & Flakes Book — Get Started with NixOS (skim)
- NixOS & Flakes Book — Enabling Flakes (read)
- NixOS & Flakes Book — Modularize Your Config (read)
Also look at nix-starter-configs — a documented template you'll probably crib from.
Commands you must know:
| Command | What it does |
|---|---|
sudo nixos-rebuild switch --flake .#hostname |
Apply config changes now |
sudo nixos-rebuild test --flake .#hostname |
Apply but don't make it the default on next boot (safer) |
sudo nixos-rebuild boot --flake .#hostname |
Apply on next boot only |
nix flake update |
Update pinned dependencies in flake.lock |
nix-collect-garbage -d |
Delete old generations + free disk |
| Pick an old generation at the boot menu | Roll back a broken rebuild |
Tip
Omit the attribute suffix in day-to-day use — both tools auto-detect the right config:
sudo nixos-rebuild switch --flake . # matches nixosConfigurations.<current hostname>
home-manager switch --flake . # matches homeConfigurations."<user>@<hostname>"nixos-rebuild uses the machine's hostname; home-manager uses both the current user
and hostname, so multiple users on the same machine each get exactly their own config.
If there's no matching entry, both commands error — they won't silently apply the wrong
config. Use the explicit --flake .#name form only for overrides or when bootstrapping
a new machine before networking.hostName is set.
The minimal nix-starter-configs template gives you:
nixos/configuration.nix # system config
nixos/hardware-configuration.nix
home-manager/home.nix # user config
This works fine for one machine and one user, but encodes three implicit assumptions that become painful to undo as your config grows:
nixos/assumes one machine. If you ever add a second (desktop alongside laptop, VM alongside bare metal), there is nowhere to put machine-specific config without restructuring everything.hosts/<name>/makes the machine an explicit dimension from day one — shared config lives outside the host directory, hardware config inside it.home-manager/assumes one user. Same problem at the user level.home/<user>/is ready for multiple users or per-machine user configs without restructuring.- No home for reusable modules.
modules/nixos/is where files live that define their ownoptionsandconfigblocks — the real NixOS module pattern. For example, agaming.nixthat exposesmy.gaming.enable = trueand conditionally enables Steam, gamemode, etc. In the flat layout you'd just have more files innixos/wired together via imports, not composable toggles.
When Nix imports a directory — modules = [./hosts/yourhostname] — it automatically
looks for default.nix inside it, the same way Node.js resolves index.js. So naming
the file default.nix lets flake.nix reference the directory without naming a specific
file, which is the Nix idiom. You can keep the name configuration.nix if you prefer —
just be explicit in flake.nix: modules = [./hosts/yourhostname/configuration.nix].
Both work; default.nix is the convention.
hosts/yourhostname/default.nix is the entry point for one specific machine — it sets
config values. modules/nixos/ is for reusable option groups that any host can toggle.
A plain host file just sets values:
# hosts/yourhostname/default.nix
{ pkgs, ... }: {
services.pipewire.enable = true;
environment.systemPackages = [ pkgs.steam ];
}A proper module declares its own options and applies config conditionally:
# modules/nixos/gaming.nix
{ lib, config, pkgs, ... }:
let cfg = config.my.gaming; in {
options.my.gaming.enable = lib.mkEnableOption "gaming setup";
config = lib.mkIf cfg.enable {
services.pipewire.enable = true;
environment.systemPackages = [ pkgs.steam pkgs.lutris ];
programs.gamemode.enable = true;
};
}Any host can then opt in with one line: my.gaming.enable = true;
When to use modules/: start with it empty. Move something there when you either
want the same config block on a second machine, or want a feature to be a named toggle
rather than always-on. For a single-machine migration, your host file can grow large —
that's fine. modules/ earns its keep when the duplication or toggleability actually
shows up.
One rename and two path changes in flake.nix:
mkdir -p hosts/yourhostname modules/nixos home/yourusername
mv nixos/configuration.nix hosts/yourhostname/default.nix
mv nixos/hardware-configuration.nix hosts/yourhostname/hardware-configuration.nix
mv home-manager/home.nix home/yourusername/default.nix
rm -rf nixos home-managerThen in flake.nix:
# nixosConfigurations
modules = [./hosts/yourhostname]; # was ./nixos/configuration.nix
# homeConfigurations
modules = [./home/yourusername]; # was ./home-manager/home.nixThe ./hardware-configuration.nix import inside your old configuration.nix keeps
working — Nix resolves it relative to the file's location, so it finds
hosts/yourhostname/hardware-configuration.nix automatically. Run git add -A before
rebuilding (the flake evaluator ignores untracked files).
Yes — it's the community consensus layout. When configs grow further, people add
overlays/ (to patch nixpkgs packages), pkgs/ (for custom derivations), and lib/
(for shared helper functions) alongside the existing structure without restructuring.
Secrets management (sops-nix or agenix) slots in as a NixOS module. Frameworks that
tried to go further (digga, early flake-parts setups) added complexity without
proportional benefit and lost momentum. hosts/modules/home has won for personal configs.
Misterio77/nix-config — the personal
config of the author of the starter template you're using. It uses exactly this layout
with 8 hosts, modules/nixos/ and modules/home-manager/ for reusable option groups,
overlays/, and pkgs/. Useful to browse when you're not sure where something should
go.
There's also this i3-kickstarter that you can use as reference
The minimal template has several placeholder comments you must resolve before a first successful build. Here's each one and what to put there.
your-hostname — attrset key in nixosConfigurations
# FIXME replace with your hostname
your-hostname = nixpkgs.lib.nixosSystem { ... };This becomes the name you pass to nixos-rebuild --flake .#yourhostname. Pick something
now — you'll use it every rebuild.
Note
Three things carry a hostname and none are linked automatically by Nix:
- This attrset key in
flake.nix - The
./hosts/<dirname>directory name (once you restructure) networking.hostNameinconfiguration.nix
The universal convention is to keep all three the same value. There's no technical
requirement, but mismatching them means constantly mapping between names in your head,
and tools that introspect nixosConfigurations (some deployment tools, scripts) assume
the key is the actual hostname. Pick one name and use it everywhere.
Note
When you get a new machine you don't replace this entry — you add a new one alongside it:
nixosConfigurations = {
myhostname = nixpkgs.lib.nixosSystem { ... }; # machine description
notus = nixpkgs.lib.nixosSystem { ... }; # future machine
};Each machine gets its own key and its own directory under hosts/. The FIXMEs are
scaffolding for the first machine only — once resolved you never touch those keys again
unless you rename or decommission a host.
"your-username@your-hostname" — attrset key in homeConfigurations
# FIXME replace with your username@hostname
"your-username@your-hostname" = home-manager.lib.homeManagerConfiguration { ... };Replace with "youruser@yourhostname". This is what
home-manager --flake .#youruser@yourhostname resolves to.
x86_64-linux — system architecture
pkgs = nixpkgs.legacyPackages.x86_64-linux; # FIXME replace x86_64-linux with your architecturex86_64-linux is correct for a standard 64-bit Intel/AMD desktop. Leave it as-is.
# FIXME: Add the rest of your current configuration
This is where your old /etc/nixos/configuration.nix content goes — boot loader, desktop
environment, packages, services. Paste your existing config here and trim the parts the
template already provides (flake/experimental-features settings, user definition, etc.)
to avoid duplicating them.
networking.hostName = "your-hostname"
Same value as the attrset key in flake.nix. Keep them in sync — a mismatch won't
break the build but will confuse you later.
your-username in users.users
The attrset key and every other your-username placeholder in this block becomes your
actual Unix login name — the same value as the username part of the homeConfigurations
key in flake.nix (the part before the @). They refer to the same Unix user; a
mismatch means home-manager won't find a config for the user NixOS just created.
initialPassword = "correcthorsebatterystaple"
Temporary — lets you log in before you set a real password. Change it with passwd
after first boot. If you're migrating an existing system rather than doing a fresh
install, you can remove this line; your existing password hash carries over.
services.openssh — remove it for a desktop workstation
The template enables SSH with key-only auth by default. You don't need it on a workstation. Remove or comment out the whole block:
# Remove the entire services.openssh blockextraGroups = ["wheel"]
wheel gives sudo. Add any groups your hardware or software needs:
| Group | When you need it |
|---|---|
networkmanager |
NetworkManager GUI / applet |
audio |
Some older apps (PipeWire usually handles this now) |
docker |
Docker without sudo |
libvirtd |
QEMU/KVM virtual machines |
plugdev |
USB devices (game controllers, Android ADB) |
home.username / home.homeDirectory
home.username = "your-username";
home.homeDirectory = "/home/your-username";Replace both with your actual username — the same value used as the users.users key in
configuration.nix and the username part of the homeConfigurations key in flake.nix.
All three refer to the same Unix user and must match. homeDirectory is always
/home/<username> for a standard Linux user.
The flake accumulates entries — you never replace an existing host or user, you add alongside it.
- Add an entry to
nixosConfigurationsinflake.nix:
nixosConfigurations = {
hostname1 = nixpkgs.lib.nixosSystem { # existing machine
specialArgs = { inherit inputs; };
modules = [ ./hosts/hostname1 ];
};
hostname2 = nixpkgs.lib.nixosSystem { # new machine
specialArgs = { inherit inputs; };
modules = [ ./hosts/hostname2 ];
};
};-
Create
hosts/hostname2/with its owndefault.nixandhardware-configuration.nix(generated on the new machine vianixos-generate-config). -
Deploy on the new machine:
sudo nixos-rebuild switch --flake .
Add an entry to homeConfigurations:
homeConfigurations = {
"alice@hostname1" = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
extraSpecialArgs = { inherit inputs; };
modules = [ ./home/alice ];
};
"bob@hostname1" = home-manager.lib.homeManagerConfiguration { # new user
pkgs = nixpkgs.legacyPackages.x86_64-linux;
extraSpecialArgs = { inherit inputs; };
modules = [ ./home/bob ];
};
};Then create home/bob/default.nix with that user's config.
The homeConfigurations key is user@host, so the same user on a different machine
is just another entry pointing at the same home/ directory:
"alice@hostname1" = home-manager.lib.homeManagerConfiguration {
modules = [ ./home/alice ]; # shared home config
};
"alice@hostname2" = home-manager.lib.homeManagerConfiguration {
modules = [ ./home/alice ]; # same directory, different host
};Both entries share one home/alice/default.nix. If the two machines need slightly
different user config, you can split it with imports inside that file.
Rather than copy-pasting the same options into each host file, move shared config into
modules/nixos/ and import it wherever needed:
# hosts/hostname1/default.nix
{ ... }: {
imports = [
./hardware-configuration.nix
../../modules/nixos/gaming.nix
];
my.gaming.enable = true;
}
# hosts/hostname2/default.nix
{ ... }: {
imports = [
./hardware-configuration.nix
../../modules/nixos/gaming.nix
];
my.gaming.enable = false;
}This is when modules/ starts paying for itself — one definition, toggled per host.
Goal: Understand the split between system config (configuration.nix) and user
config (home.nix), and why most of your dotfiles and user packages should live in the
latter.
Read:
- NixOS & Flakes Book — Getting Started with Home Manager
- Home Manager Manual — intro (just the first page)
- Home Manager Options — bookmark this; it's your lookup table for
programs.zsh.*,programs.neovim.*, etc.
The key split:
configuration.nix→ kernel, drivers, services, system users, networking, desktop environment. Stuff that needs root.home.nix(home-manager) → your shell, editor, git config, fonts you use, most of your CLI tools, dotfiles. Stuff that runs as you.
A practical case where modules/ pays off immediately: supporting both GNOME and a
window manager like Hyprland in the same config, with a single toggle to switch between
them.
The simplest approach. Add both to your host config and GDM lets you choose the session at the login screen — no rebuild needed to switch:
# hosts/<hostname>/default.nix
services.xserver.displayManager.gdm.enable = true;
services.xserver.desktopManager.gnome.enable = true;
programs.hyprland.enable = true;Good for experimenting. Downside: both are always installed.
Create one module per desktop in modules/nixos/:
# modules/nixos/gnome.nix
{ lib, config, ... }:
let cfg = config.my.desktop.gnome; in {
options.my.desktop.gnome.enable = lib.mkEnableOption "GNOME";
config = lib.mkIf cfg.enable {
services.xserver.enable = true;
services.xserver.displayManager.gdm.enable = true;
services.xserver.desktopManager.gnome.enable = true;
};
}
# modules/nixos/hyprland.nix
{ lib, config, ... }:
let cfg = config.my.desktop.hyprland; in {
options.my.desktop.hyprland.enable = lib.mkEnableOption "Hyprland";
config = lib.mkIf cfg.enable {
programs.hyprland.enable = true;
services.xserver.displayManager.gdm.enable = true;
};
}Then in your host config, import both modules and flip the toggle:
# hosts/<hostname>/default.nix
imports = [
./hardware-configuration.nix
../../modules/nixos/gnome.nix
../../modules/nixos/hyprland.nix
];
my.desktop.gnome.enable = true;
# my.desktop.hyprland.enable = true;Each desktop has user-level config too — GNOME extensions and dconf settings, or
Hyprland config, waybar, rofi, etc. Mirror the same pattern in modules/home/:
# modules/home/gnome.nix
{ lib, config, pkgs, ... }:
let cfg = config.my.desktop.gnome; in {
options.my.desktop.gnome.enable = lib.mkEnableOption "GNOME home config";
config = lib.mkIf cfg.enable {
dconf.settings = {
"org/gnome/desktop/interface" = { color-scheme = "prefer-dark"; };
};
home.packages = with pkgs; [ gnomeExtensions.pop-shell ];
};
}
# modules/home/hyprland.nix
{ lib, config, pkgs, ... }:
let cfg = config.my.desktop.hyprland; in {
options.my.desktop.hyprland.enable = lib.mkEnableOption "Hyprland home config";
config = lib.mkIf cfg.enable {
wayland.windowManager.hyprland.enable = true;
programs.waybar.enable = true;
home.packages = with pkgs; [ rofi-wayland dunst ];
};
}Wire them into your home config the same way:
# home/<username>/default.nix
imports = [
../../modules/home/gnome.nix
../../modules/home/hyprland.nix
];
my.desktop.gnome.enable = true;
# my.desktop.hyprland.enable = true;One toggle in each file — system and home — and a rebuild switches everything cleanly. Start with Option A while exploring, graduate to Option B once you've settled.
Goal: Instead of having every language toolchain globally installed, each project
declares what it needs in a flake.nix or shell.nix, and direnv auto-loads it when
you cd in.
Read:
This replaces most of what nvm, rustup, and ghcup do for you today — but you can
also keep using those on NixOS if that's easier. Both are valid. More on this in the
checklist.
- NixOS Manual — the canonical reference.
- NixOS Wiki — community recipes for specific hardware, desktop environments, apps. Search here first when you get stuck.
- search.nixos.org — find package names.
- search.nixos.org/options — find NixOS options.
- MyNixOS — nicer-looking alternative search for packages and options.
You do not need to learn these to migrate:
- Writing your own derivations /
mkDerivation - Overlays (beyond recognising them when you copy-paste)
- Nixpkgs internals
- NixOS testing framework
- Cross-compilation
- Remote deployments (Colmena, NixOps, deploy-rs)
Come back to these when you actually need them.
Full list of resources linked above, in the order they appear. Every inline link resolves to one of these.
- Zero to Nix — Concepts
- Zero to Nix — Nix
- Zero to Nix — NixOS
- Zero to Nix — Flakes
- Zero to Nix — The Nix store
- nix.dev — Nix language basics
- NixOS & Flakes Book — Get Started with NixOS
- NixOS & Flakes Book — Enabling Flakes
- NixOS & Flakes Book — Modularize Your Config
- nix-starter-configs (Misterio77)
- NixOS & Flakes Book — Getting Started with Home Manager
- Home Manager Manual
- Home Manager Options
- nixos.asia — Nix for Development
- nixos.asia — direnv
- NixOS Manual (stable)
- NixOS Wiki
- search.nixos.org/packages
- search.nixos.org/options
- MyNixOS
- NixOS Discourse — Unofficial NixOS & Flakes book announcement
Yes, this is AI generated