Linux

NixOS — Notes for First-Timers

Contents

NixOS keeps coming up, so I looked into what it actually is. I did not install NixOS in a VM. I installed the Nix package manager on an Ubuntu 24.04 server and went as far as building a NixOS configuration. Versions: NixOS 26.05, Nix 2.35.3.

What it is

A Linux distribution built on top of the Nix package manager.

On a normal distro you install the OS, then add packages and edit files under /etc until the server looks right. On NixOS you describe the whole system in one file, configuration.nix, and the OS is built from it. Which packages are installed and which services run is all in that file.

  • Nix started around 2003 as Eelco Dolstra’s PhD research
  • Two releases a year, May and November, named year.month
  • Current release: 26.05 “Yarara” (May 30, 2026). Next: 26.11 “Zokor”
  • Each release is supported for seven months. 26.05 runs until December 31, 2026

You can use the Nix package manager without NixOS, on Ubuntu or macOS for example.

How it differs from a normal distro

  • Where packages live: Ubuntu uses /usr/bin and /usr/lib. NixOS uses /nix/store/<hash>-<name>-<version>
  • Configuration: on Ubuntu you edit files under /etc. On NixOS you edit configuration.nix and rebuild
  • Several versions of one package: hard on Ubuntu. On NixOS they just sit side by side
  • An upgrade breaks things: on Ubuntu you restore from backup. On NixOS you roll back to the previous generation, also from the boot menu
  • Identical servers: Ubuntu needs Ansible or similar. On NixOS the same file gives the same system

Below is what I actually tried.

Install

The usual way is the multi-user (daemon) install.

1
sh <(curl -L https://nixos.org/nix/install) --daemon

I was root inside a container, so I used --no-daemon. As root it fails because the nixbld group is missing; adding build-users-group = to /etc/nix/nix.conf and rerunning fixed it.

The newer commands (nix run, nix shell) and flakes are still experimental and have to be turned on.

1
2
# /etc/nix/nix.conf
experimental-features = nix-command flakes

Without it:

1
error: experimental Nix feature 'nix-command' is disabled; add '--extra-experimental-features nix-command' to enable it

Packages live in /nix/store

The server already had Python 3.11. I ran 3.12 and 3.13 through Nix.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ python3 --version
Python 3.11.15

$ nix shell nixpkgs/nixos-26.05#python312 -c python3 --version
Python 3.12.14
/nix/store/6jmqjw0f3s7kl7x4ps1abab1dpaxp3yj-python3-3.12.14/bin/python3.12

$ nix shell nixpkgs/nixos-26.05#python313 -c python3 --version
Python 3.13.15
/nix/store/jy00n3jp5xj8s40n1fbldmkcr5n7avxc-python3-3.13.15/bin/python3.13

$ python3 --version
Python 3.11.15
  • Each version gets its own path under /nix/store, so they never collide
  • The hash comes from everything that went into the build (source, dependencies, options). Change any of it and the path changes
  • The system’s own 3.11 is untouched

The first fetch for Python 3.12 pulled 23 paths (74.7 MB). Python 3.13 shared most of them and needed only one more path (54.1 MB).

Use a tool without installing it

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ nix shell nixpkgs/nixos-26.05#cowsay -c cowsay nix
 _____
< nix >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

$ which cowsay
(nothing)

It runs once and leaves nothing on the system. Handy for per-project tools.

Rollback

Every install creates a new generation. I installed hello, then jq.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
$ nix profile add nixpkgs/nixos-26.05#hello
$ nix profile add nixpkgs/nixos-26.05#jq
$ jq --version
jq-1.8.2

$ nix profile history
Version 1 (2026-10-21):
  nix: ∅ -> 2.35.3

Version 2 (2026-10-21) <- 1:
  flake:nixpkgs/nixos-26.05#legacyPackages.x86_64-linux.hello: ∅ -> 2.12.3

Version 3 (2026-10-21) <- 2:
  flake:nixpkgs/nixos-26.05#legacyPackages.x86_64-linux.jq: ∅ -> 1.8.2-bin, 1.8.2-man

One step back:

1
2
3
4
5
$ nix profile rollback
switching profile from version 3 to 2

$ jq --version
jq-1.7

The Nix jq 1.8.2 is gone from the path and Ubuntu’s own jq 1.7 shows up again. Nothing was deleted. The files are still in /nix/store, so going forward again is instant.

1
2
3
4
5
$ nix profile rollback --to 3
switching profile from version 2 to 3

$ jq --version
jq-1.8.2

On NixOS this works for the whole OS. Every system change is a generation and stays in the boot menu, so if an update leaves the machine unbootable you pick the previous one. I could not try that part without a real NixOS install.

The whole system in one file

A config with nginx, SSH and one user:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# configuration.nix
{ config, pkgs, ... }:

{
  boot.loader.grub.device = "/dev/sda";
  fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };

  networking.hostName = "paper-test";
  time.timeZone = "Asia/Seoul";

  users.users.paper = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
  };

  environment.systemPackages = with pkgs; [ git vim htop ];

  services.openssh.enable = true;
  services.nginx.enable = true;
  networking.firewall.allowedTCPPorts = [ 22 80 ];

  system.stateVersion = "26.05";
}

On NixOS you put this in /etc/nixos/configuration.nix and apply it with:

1
sudo nixos-rebuild switch

On Ubuntu I can only build it:

1
nix-build -I nixpkgs=channel:nixos-26.05 -I nixos-config=./configuration.nix '<nixpkgs/nixos>' -A system
  • First build: 465 paths fetched (487.5 MB), 270 built, 107 seconds
  • Result is 1.2 GB, with kernel 6.18.57

The result folder holds the kernel, the initrd and /etc.

1
2
3
4
5
$ ls result
activate  bin  boot.json  dry-activate  etc  firmware  init  initrd  kernel  kernel-modules  ...

$ cat result/nixos-version
26.05.12297.cd4ea8d57de1

Even the nginx config is not a file in /etc/nginx. It is generated in the store and the service points straight at it:

1
ExecStart=/nix/store/smpkfhyjj22b4zy1f0cprzfhr0nimllr-nginx-1.30.6/bin/nginx -c '/nix/store/biwy2jjqy6gvw5k9j41fg3c2y1kv996v-nginx.conf'

So you don’t log in and edit config files by hand. You change configuration.nix and rebuild.

Only the change gets rebuilt

I added tmux to the package list and rebuilt. 11 seconds.

1
2
3
4
$ nix store diff-closures ./result ./result2
libutempter: ∅ → 1.2.3, 38.6 KiB
tmux: ∅ → 3.7b, 1.4 MiB
utf8proc: ∅ → 2.11.3, 393.5 KiB

Only tmux and the two libraries it needs were added. Everything else was reused.

A typo

I changed enable to enabel:

1
2
error: The option `services.nginx.enabel' does not exist. Definition values:
- In `/.../typo.nix': true

It fails at build time, before anything is applied. The real error comes after a long stack trace, which is confusing at first.

What is hard at first

The Nix language

The config is written in Nix, its own language. The syntax is small but takes time to get used to, and error messages are long, as above.

Binaries from other distros don’t just run

A normal Linux binary looks for /lib64/ld-linux-x86-64.so.2. Anything built by Nix points into the store instead:

1
2
3
4
5
# Ubuntu /usr/bin/ls
interpreter: /lib64/ld-linux-x86-64.so.2

# jq from nix
interpreter: /nix/store/fsd4vbp9zjd9iaicq2fb9lpc1h5j973y-glibc-2.42-100/lib/ld-linux-x86-64.so.2

NixOS has no real /lib64 loader. In 26.05 environment.stub-ld.enable is on by default, so you get an explanatory message instead. To run outside binaries, the usual answer is programs.nix-ld.enable = true. I did not try it.

Disk space

After only these tests /nix/store was 1.1 GB. Old generations keep piling up until you clean them:

1
nix-collect-garbage -d

Other

  • Documentation is spread across the manual, nix.dev and the wiki
  • From 26.05 the early boot stage (initrd) is systemd-based by default, so older guides may not match

Trying it

  • Full NixOS: ISO from https://nixos.org/download/ (graphical installer or minimal)
  • Lower commitment: install just Nix on your current Linux or Mac and start with nix shell
  • Intel Macs: 26.05 is the last release with packages built for them

References