Post #nix

NixOS, BTW

I really like Nix, and think you should use it!

Aug 14, 2024 10 minute read

I’m not a Nix expert.

I’ve been using it for years and I still couldn’t write you a derivation from memory. I google the syntax constantly. I have broken my system in ways that a native rollback didn’t fix. I have stared at incomprehensible error messages at 2am while my laptop refused to boot.

I am here to tell you that it was worth it.

This is the honest pitch. The kind you’d give over tea, not a lecture.

I barely know Nix but I’ve used it in anger long enough to know why I love it.

Four Nixes

“Nix” is one word for four different things. They really should have picked separate names.

  1. There’s nix, the package manager.

    Like brew or apt, but functional. Packages are built from descriptions called derivations. The output is cached. If the inputs haven’t changed, the build is skipped. You get exact reproducibility by default,

  2. And nix, the language.

    A dynamically typed, functional, lazily evaluated DSL that you use to write those derivations. It looks like JSON had a baby with Haskell and the baby was raised by a build system. It’s weird. You get used to it.

  3. There’s also nixpkgs. The package repository.

    One of the largest in existence. If it compiles on Linux, it’s probably in nixpkgs. If it’s not, writing a derivation for it is usually a few lines.

  4. And finally, NixOS. A Linux distribution built entirely on Nix.

    There is no /usr/bin. There is no apt. Your entire system — packages, services, users, kernel modules, firewall rules — is declared in a single config file. Rebuild. Reboot. That’s your OS.

The four things are inseparable.

You can’t use NixOS without learning nix the language.

You can’t use nix the package manager without understanding derivations.

They all feed into each other. This is the learning curve.

The Kool-Aid

I used to be a Windows “ricer” all in on:

I spent years making Windows look like anything but itself.

Then I became a distro hopper: Arch, Fedora, Ubuntu, Manjaro, Void. Each one had the same problem: I’d configure everything perfectly, something would break, and I’d have no idea how to get back to where I was.

The config was a transient thing in my head.

A colleague once roasted me, saying I “only had a single braincell”…

I can’t keep up with transient state in my brain.

NixOS fixes this at the OS level. My entire machine: packages, dotfiles, services, kernel parameters, lives in a single flake.

# my laptop, iroha
{
system = "x86_64-linux";
desktop = "gnome";
modules = {
tui.enable = true;
gui.enable = true;
};
}

That’s it. That’s the entire machine definition.

Rebuild and I have a laptop with GNOME, all my terminal tools, my GUI apps, my fonts, my themes. Everything.

If something breaks, I roll back. NixOS keeps every previous generation on disk. Boot into the last working one. Keep working. Fix it later.

I ran out of disk space mid-nixos-rebuild switch once.

The system broke and the native rollback didn’t save me because the build was half-finished and the generation was corrupted.

That’s a Linux problem, not a NixOS problem though.

The Store

How does any of this actually work? Nix throws out the traditional filesystem model entirely.

Every package you install lives at a hashed path under /nix/store/. The hash encodes the entire build recipe: source tarball, compiler version, every library it links against, every build flag.

Change any one of those and you get a different hash (which look like the following):

/nix/store/3lq1wyhq4j0xkxifyz9irw0fqabc1234-git-2.42.0/.

Same hash on two different machines means bit-for-bit identical binaries.

Three superpowers emerge out of this simple idea:

  1. Reproducibility.

    Same inputs, same outputs, every time, on any machine.

  2. Isolation.

    Packages never step on each other. Your system doesn’t accumulate cruft from every install you’ve ever done.

  3. Rollbacks.

    Your current environment is just a set of symlinks into the store. Point them at a previous generation and you’re back.

Try running nix-shell -p <package>.

It drops you into a transient shell with a one-off tool installed. Your system stays untouched you that you can try things without committing.

Flakes Fix It

Nix used to use a thing called a “channel” which was globally managed state.

Update your channel, everything updates at once. It also meant it was hard to pin a specific version of a package, and hard to know what exact version of a package you were using.

Flakes fixed it.

A flake.nix declares inputs explicitly and generates a flake.lock.

Deterministic, reproducible, no global state. Two people with the same lock file get identical environments.

Pair it with direnv and you auto-enter your dev shell whenever you cd into a project. A minimal project flake looks like:

{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
outputs = { self, nixpkgs }: {
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell {
buildInputs = [ nixpkgs.elixir nixpkgs.postgresql ];
};
};
}

This says: pin nixpkgs to a specific revision, and when someone enters this project’s dev shell, give them Elixir and PostgreSQL.

No “which version of Elixir?”

No brew update updating the universe surprises.

Unify stuff like apt and brew and asdf and rvm and pyenv. All in one.

You don’t have to go full NixOS to benefit from this, either.

Install the package manager, write a flake.nix for your project, add direnv. That alone replaces every version manager you’ve ever been annoyed by.

Nix for Teams

For years, my Nix config was personal. Hacky. Minimal.

A flake.nix that only I had to understand. Comments were sparse because I knew what everything did, I only needed to support Linux, the tools I personally used, etc.

It was relatively easy to convince team members to start using Nix. Trying it out, at least.

But suddenly, the flake had to be maintainable. Had to be documented. Had to handle edge cases I’d been ignoring because I was the only user.

I had to go from “this works for me” to “this works for everyone” and that transition was genuinely harder than learning Nix in the first place.

Nix, my Way

After years of this, I’ve settled on a pattern that I genuinely like.

It follows the “Dendendric Nix” pattern.

The idea is simple: every file is a top-level module. File paths name features. As flat of a namespaces that works.

modules/
os/
compose.nix # docker-compose support
services/
openclaw.nix # openclaw agent
media-server.nix # plex + jellyfin
minecraft.nix # minecraft server
home/
tui.nix # terminal tools
gui.nix # desktop apps

Each module declares its own options. Each host imports the modules it needs and enables them with a clean declarative block.

# hosts/linux/kyubey/services.nix
{
modules = {
tailscale.enable = true;
serve.enable = true;
minecraft.enable = true;
media-server.enable = true;
};
}

That’s my server. Tailscale for networking, serve for a reverse proxy, Minecraft, and Plex + Jellyfin.

Everything else like firewall rules, systemd services, backup timers is handled inside the modules themselves. Users don’t have to think about it.

For services that run in Docker, I use an os.compose abstraction where Docker Compose files get converted to Nix options automatically.

# hosts/linux/madoka/services.nix
{
os.compose.openclaw = {
enable = true;
backup.enable = true;
openFirewall = true;
extraPackages = [ pkgs.curl ];
sessionVariables = {
OPENCLAW_CONFIG = "/etc/openclaw/config.toml";
};
};
}

This is the part where Nix actually delivers on the promise.

The abstraction pays off.

I add a service by writing five lines. I don’t have to remember which ports to open or which directories to create.

The config is the documentation.

Bootstrapping a new machine is three commands. Ten minutes and I have my entire environment back.

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install --determinate
nix-shell -p git
git clone https://github.com/vereis/nix-config && cd nix-config && sudo nixos-rebuild switch --flake .#$HOSTNAME

It still feels like magic :)

Erase Your Darlings

There’s a technique Graham Christensen wrote about called “Erase Your Darlings”.

You set up your root partition so that it gets wiped on every boot. A blank snapshot and a ZFS rollback, and / is fresh each time.

You explicitly opt in to what state you keep:

  • Wireguard keys in /persist.
  • SSH host keys in /persist.
  • Everything else? Gone.

The first time I tried this it was genuinely scary.

What was I about to lose on the next reboot? Nothing, it turned out.

Everything I cared about was already tracked, or stored somewhere.

Home Manager

I used to use Home Manager for user-level config like dotfiles, shell setup, editor plugins.

It lives in the same repo as everything else, and is basically “what if NixOS managed your dotfiles too?”

I’m transitioning away from it, though.

Nix has built-in ways to create symlinks and I’d rather use those than pull in a whole extra abstraction layer.

Flakes make it easier to extend nixpkgs and do other things too so… the less indirection between me and my config, the better.

If you’re just getting started, definitely look into Home Manager, it is great. It handles user-level config declaratively and integrates cleanly with NixOS.

I’m just at the point where I have opinions, and Home Manager ain’t it.

The Learning Curve

I won’t pretend the curve isn’t real.

It took me multiple false starts. I’d install NixOS, get frustrated with the syntax, go back to Arch, miss the reproducibility (or break my GPU drivers again), come back.

Rinse and repeat for about a year before it stuck.

The error messages are still bad. The documentation is still… uneven. The language is still weird.

You will google things constantly. You will copy-paste configs from strangers on GitHub and pray they work.

But once it clicks, the value is undeniable.

My entire computing environment: four machines, dozens of services, hundreds of packages, is declared in one repo where:

  • I can reproduce any machine from scratch in under ten minutes.
  • I can experiment fearlessly because rollbacks are free.
  • I can share configs between machines without copy-pasting.
  • I can manage machines remotely, servers, or for family and friends, all from a single flake.

Nix makes Linux not hurt. That’s the pitch. Everything else is implementation details.

Start small. Install the package manager. Write a flake.nix for one project. Let direnv handle your dev environments.

You don’t have to go full NixOS on day one. But if you do someday, that’s where things get really fun.

Nix is not easy. It is good. The two things are different and the distinction matters.

Directory

1
  1. 01.
    Posts

    No description yet. Typical mysterious little page.

    (no tags yet)