Skip to content

Instantly share code, notes, and snippets.

@OlaoluwaM
Created May 18, 2026 19:24
Show Gist options
  • Select an option

  • Save OlaoluwaM/ed6b813e2f948179cfa4c6fb6e3b5d7d to your computer and use it in GitHub Desktop.

Select an option

Save OlaoluwaM/ed6b813e2f948179cfa4c6fb6e3b5d7d to your computer and use it in GitHub Desktop.
NixOS Learning Guide

NixOS Learning Path

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.


Phase 0 — The mental model (before you do anything)

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:

  1. Your system is a function of a config file. You edit a .nix file, run nixos-rebuild switch, and NixOS makes your machine match the file. You don't dnf install things imperatively anymore — you add them to the file.
  2. 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.
  3. 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 why FHS assumptions (bare ./configure && make install, random downloaded binaries) often don't "just work".

Phase 1 — The Nix language (just enough)

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 something
  • import ./other-file.nix brings another file in
  • with pkgs; [ neovim git ] is a shortcut that saves typing pkgs. everywhere

Don't try to memorise the semantics of let ... in, overlays, or overrides yet. They'll click later when you actually need them.


Sidebar — Why flakes at all?

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:

  1. The classic way/etc/nixos/configuration.nix + the nix-channel command. Packages come from a "channel" (a moving pointer to a version of nixpkgs). When you run nix-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.
  2. Flakes — your config is a flake.nix that lists its inputs (nixpkgs, home-manager, etc.) by URL, and a flake.lock file 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 runs nixos-rebuild gets the exact same system.

The three concrete wins

  1. Reproducibility, by default. The flake.lock is 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.
  2. One standard structure. Any flake has inputs (what it depends on) and outputs (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 with default.nix, shell.nix, overlays/, etc.
  3. 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.

The honest caveat

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.


Phase 2 — Flakes and nixos-rebuild (the daily workflow)

Goal: Understand how a flake-based NixOS config is laid out and how you rebuild.

Read, in order:

  1. NixOS & Flakes Book — Get Started with NixOS (skim)
  2. NixOS & Flakes Book — Enabling Flakes (read)
  3. 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.


Sidebar — Config layout: minimal template vs hosts/modules/home

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 own options and config blocks — the real NixOS module pattern. For example, a gaming.nix that exposes my.gaming.enable = true and conditionally enables Steam, gamemode, etc. In the flat layout you'd just have more files in nixos/ wired together via imports, not composable toggles.

Why default.nix and not configuration.nix?

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.

What modules/ is actually for

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.

Migrating from minimal to hosts/modules/home

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-manager

Then in flake.nix:

# nixosConfigurations
modules = [./hosts/yourhostname];    # was ./nixos/configuration.nix

# homeConfigurations
modules = [./home/yourusername];     # was ./home-manager/home.nix

The ./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).

Is hosts/modules/home flexible long-term?

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.

Real-world reference

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


Sidebar — Filling in the template (addressing the FIXMEs)

The minimal template has several placeholder comments you must resolve before a first successful build. Here's each one and what to put there.

flake.nix

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:

  1. This attrset key in flake.nix
  2. The ./hosts/<dirname> directory name (once you restructure)
  3. networking.hostName in configuration.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 architecture

x86_64-linux is correct for a standard 64-bit Intel/AMD desktop. Leave it as-is.

hosts/<hostnmae>/default.nix

# 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 block

extraGroups = ["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>/default.nix

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.


Sidebar — Extending the config for new machines and users

The flake accumulates entries — you never replace an existing host or user, you add alongside it.

Adding a new machine

  1. Add an entry to nixosConfigurations in flake.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 ];
  };
};
  1. Create hosts/hostname2/ with its own default.nix and hardware-configuration.nix (generated on the new machine via nixos-generate-config).

  2. Deploy on the new machine: sudo nixos-rebuild switch --flake .

Adding a new user on an existing machine

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.

Same user on a new machine

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.

Sharing config between hosts via modules

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.


Phase 3 — Home Manager (for your dotfiles and user-level stuff)

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:

  1. NixOS & Flakes Book — Getting Started with Home Manager
  2. Home Manager Manual — intro (just the first page)
  3. 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.

Sidebar — Switching desktop environments (a real-world module example)

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.

Option A — Install both, pick at login

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.

Option B — Module toggles (rebuild to switch, cleaner)

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;

Mirroring in home-manager

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.


Phase 4 — Dev shells and direnv (how you do project work)

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:

  1. nixos.asia — Nix for Development
  2. nixos.asia — direnv

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.


Phase 5 — Reference material (dip in as needed, don't read cover-to-cover)


What to skip (for now)

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.


Sources

Full list of resources linked above, in the order they appear. Every inline link resolves to one of these.

  1. Zero to Nix — Concepts
  2. Zero to Nix — Nix
  3. Zero to Nix — NixOS
  4. Zero to Nix — Flakes
  5. Zero to Nix — The Nix store
  6. nix.dev — Nix language basics
  7. NixOS & Flakes Book — Get Started with NixOS
  8. NixOS & Flakes Book — Enabling Flakes
  9. NixOS & Flakes Book — Modularize Your Config
  10. nix-starter-configs (Misterio77)
  11. NixOS & Flakes Book — Getting Started with Home Manager
  12. Home Manager Manual
  13. Home Manager Options
  14. nixos.asia — Nix for Development
  15. nixos.asia — direnv
  16. NixOS Manual (stable)
  17. NixOS Wiki
  18. search.nixos.org/packages
  19. search.nixos.org/options
  20. MyNixOS
  21. NixOS Discourse — Unofficial NixOS & Flakes book announcement
@OlaoluwaM

Copy link
Copy Markdown
Author

Yes, this is AI generated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment