Andrei Mahalean's Weblog

Nixos Part 1

· Andrei Mahalean

Why NixOS?

Series: My NixOS Homelab: Part 1 of 10

TL;DR: Traditional Linux servers accumulate changes over time until no one really knows what they’re running. NixOS fixes this by making your entire system configuration a file you can read, commit to git, and roll back at will. The learning curve is steep and the docs aren’t great, but it’s worth it. This series is the story of how I got here.


How I Got Here

My homelab went through a few iterations before landing on NixOS.

First it was the obvious thing: install Ubuntu, apt install everything, configure it by hand. It worked. For a while.

Then I moved to Docker Compose. Services were more portable, easier to version, easier to reproduce. Better, but the host OS was still a snowflake. Every tweak I’d made to the underlying system was stored only in my memory and a pile of shell history.

Then I tried ansible-nas, which at least acknowledged the problem. The idea was right: describe your homelab as code, run a playbook, get a configured system. But it still felt wrong. Ansible describes actions to take, not the state you want. Run the same playbook twice and you’re hoping the idempotency checks hold. The underlying system could still drift between runs.

The moment that crystallised it: a disk died, and I had to rebuild from scratch using ansible-nas. The playbook ran. But it wasn’t a complete picture of what I’d had. Things needed fixing manually. I can’t even remember exactly what. Which is precisely the problem. If you can’t remember what your system needs to be correct, you don’t have a declarative system. You have a script with amnesia.

There had to be a better way.


I Tried NixOS Before. It Didn’t Stick.

I’d actually tried NixOS years earlier, on a laptop. It did not go well.

The learning curve was brutal. The documentation was (and honestly, still is) not great. Nix the language has a unique syntax that nothing prepares you for. And the whole Flakes situation (the feature that makes NixOS properly reproducible) was labelled “experimental”, which at the time made me nervous enough to avoid it. I gave up and went back to something that just worked.

What changed was a HN thread where a bunch of people made the case that NixOS pairs particularly well with LLMs. The argument: because NixOS config is a declarative language with a massive, well-structured package repository, an LLM can actually reason about it (I am aware LLMs don’t actually think but for the sake of the conversation, we’ll go with that word). It can look up available options in nixpkgs, write correct module config, and catch mistakes in ways that are much harder with imperative shell scripts.

I decided to try again, this time leaning into that. The workflow I landed on: plan changes with Claude Opus (for the design thinking), implement with Claude Sonnet (for the actual Nix config), review the diff myself before applying. This is not a post about AI-assisted infrastructure, but it’s worth being honest that the tooling is part of why it finally clicked.

NixOS: Your Entire System Is a File

NixOS takes a radically different approach to the problem. Instead of running commands that mutate system state, you declare what your system should look like in a set of configuration files. Then you apply that configuration, and NixOS makes it so.

The entire state of my server (every installed package, every service, every systemd unit, every user, every open port) is described in a git repository. To understand what my server is running, I read the config. To change something, I edit a file and run nixos-rebuild switch. To undo that change, I run nixos-rebuild switch --rollback and I’m back to exactly where I was before.

It sounds almost too good, so let me be clear: NixOS is not easy. The learning curve is real. The Nix language takes getting used to. The ecosystem has rough edges. There are times you’ll spend an afternoon solving a problem that would’ve taken five minutes on Ubuntu.

But once it clicks, it changes how you think about infrastructure. Going back to imperative config feels like giving up something important.

The Mental Model Shift

Before NixOS makes sense, you need to internalise one idea:

NixOS doesn’t modify your system. It builds a new one and switches to it.

When you run nixos-rebuild switch, Nix evaluates your configuration, builds every package and config file from scratch in an immutable store (/nix/store), and then atomically switches the running system to that new generation. Your previous configuration still exists. It’s still in the boot menu. You can switch back to it instantly.

This has a few profound consequences:

There is no “system state” outside your config. Anything you set up by hand outside of your Nix config doesn’t survive a rebuild. This sounds punishing at first. It’s actually liberating. It forces you to commit changes to config rather than letting them drift into the void.

Every package is content-addressed and isolated. Two packages can depend on different versions of a library, both installed simultaneously, with zero conflict. The infamous “dependency hell” problem is just… gone.

Rollbacks are instantaneous. Because each nixos-rebuild switch produces a new system generation and the old one is kept, rolling back means rebooting and picking the previous entry from GRUB, or running nixos-rebuild switch --rollback without even rebooting.

Reproducibility is built in. With Nix Flakes (which we’ll use from day one in this series), every input to your system (nixpkgs itself, every community module, every tool) is pinned to an exact commit hash. Your flake.lock is a complete bill of materials for your system. Check it into git and you can rebuild the exact same system six months from now on entirely different hardware.


Why Not Just Use Ansible?

Fair question. I tried that (via ansible-nas, specifically).

Ansible brings idempotence and repeatability, but it’s still fundamentally imperative. It describes actions to take, not the desired state of the system. Run the same playbook twice and you’re hoping the authors wrote proper idempotency checks everywhere. The underlying system packages are still managed by the distro’s package manager and can drift between runs. When I had to rebuild after a disk failure, ansible-nas got me close, but not all the way there.

Docker Compose / Podman solves the “which version of this app” problem for containerised workloads, but your host system is still a snowflake. The kernel, system packages, networking configuration, secrets management. All of that lives outside the containers and can drift.

NixOS manages everything: the host OS, the kernel, system services, user packages, dotfiles via Home Manager, and containers too if you want them. The whole stack.


What We’re Building: Meet bunk

Throughout this series, I’ll be working with a real machine: a homelab server called bunk.

Hardware:

Services running (all declared in Nix config):

Category Services
Media Jellyfin, Sonarr, Radarr, Prowlarr, Bazarr, NZBGet, Transmission
Photos Immich (with ML face recognition)
Documents Paperless-ngx
Books Calibre-Web, Shelfmark
Finance Actual Budget
Productivity Vikunja, Mealie, n8n
Monitoring Uptime Kuma, Scrutiny
Infrastructure Caddy, Authelia, Podman, NFS, Restic, BorgBackup

That’s 30+ services, all declared in Nix, all backed up to two separate cloud providers. The system configuration lives in a git repository and rebuilding from scratch is one command.

One honest caveat: the system is declarative, but application-level configuration is not always. Some services can be configured entirely through NixOS module options. Some accept environment variables. Some require you to click through a setup wizard on first run and store their state in a database. I think of it in three tiers: NixOS module options where possible, environment variables for secrets and simple settings, and Click-Ops as a last resort. The Click-Ops stuff is backed up offsite and I can roll back the system around it, so I’m comfortable with that trade-off. I revisit it once a year to see if anything can move up the chain.

Not all services support SSO either. Some won’t let you disable their built-in auth at all, and others lack OIDC support entirely. For those, Authelia sits out and the app handles its own authentication. It’s not perfectly uniform, but it works.


The Core Concepts You’ll Need

Before diving into installation in Part 2, here are the key ideas that will come up repeatedly:

Nix (the language)

A purely functional, lazily-evaluated language used to describe packages and configurations. It has an unusual syntax that takes some getting used to. You don’t need to master it to be productive, but you do need to be comfortable reading it.

# A simple NixOS service declaration
services.caddy = {
  enable = true;
  virtualHosts."jellyfin.maha.nz".extraConfig = ''
    reverse_proxy localhost:8096
  '';
};

Nixpkgs

One of the largest package repositories in existence, with over 100,000 packages. It’s also the source of NixOS module definitions: those services.* and programs.* options that let you configure system services declaratively.

Nix Flakes

An experimental (but widely adopted) feature that pins all your Nix inputs to exact versions, making builds fully reproducible. We’ll use flakes from day one. Think of flake.nix as package.json and flake.lock as package-lock.json, but for your entire operating system.

Home Manager

A Nix-based tool for managing user-level configuration (dotfiles, user packages, shell config). We’ll use it as a NixOS module so user and system config live in the same git repo.

Generations

Each time you run nixos-rebuild switch, NixOS creates a new generation (a snapshot of your system configuration). You can list them with nix-env --list-generations, boot into any of them from the bootloader, and roll back instantly. Old generations are garbage-collected when you run nix-collect-garbage.


The Trade-offs (Let’s Be Honest)

NixOS is not for everyone. Here’s what you’re signing up for:

The Nix language is genuinely weird. It’s not Python or YAML or HCL. It’s a functional language with lazy evaluation, and some concepts (like lib.mkIf, lib.optionalAttrs, overlays) take real effort to understand if you are not that smart like me.

Error messages can be cryptic. When your config fails to evaluate, the error output is sometimes helpful and sometimes looks like it was generated by a Turing machine having an existential crisis.

The ecosystem moves fast. Nixpkgs is massive and well-maintained, but NixOS-specific modules vary in quality. Some services have beautifully-designed modules with every option exposed. Others have bare-bones modules that still require manual workarounds.

First-time setup is slower. Doing what takes ten minutes on Ubuntu might take an hour on NixOS the first time, as you figure out the right module option or why your service won’t start.

The payoff is real. After the initial investment, day-to-day operations become remarkably smooth. Adding a new service is adding a few lines to a config file. Upgrading the whole system is nix flake update && nixos-rebuild switch. Breaking something is a five-second rollback.


What’s Coming in This Series

Here’s the roadmap:

  1. Part 1 (this post): Why NixOS?
  2. Part 2: Installing NixOS with Flakes and ZFS from day one
  3. Part 3: Structuring a config that doesn’t fall apart
  4. Part 4: Secrets management with agenix (encrypted config in git)
  5. Part 5: Caddy + Authelia: SSO for every service
  6. Part 6: Self-hosting a media stack the NixOS way
  7. Part 7: Running containers when NixOS modules don’t exist
  8. Part 8: Backups with Restic + BorgBackup
  9. Part 9: Monitoring, alerting, and knowing when things break
  10. Part 10: Deploying, updating, and rolling back without fear

Each article is written to be useful on its own, but they build on each other. If you’re starting from scratch, I’d recommend reading in order. If you’re already running NixOS and just want to know how I’ve handled secrets or backups, jump ahead.

The config repo is private, but all the relevant examples will be in the posts themselves. I’ll include real snippets throughout the series.


Should You Use NixOS?

If any of these sound familiar, NixOS is probably worth your time:

If you just want to run a few containers and don’t want to invest in learning a new paradigm, stick with Docker Compose on Ubuntu. That’s a completely valid choice.

A word on the popular alternatives. Kubernetes comes up a lot in homelab circles. I have no interest in running it at home. It’s a single host, I don’t need high availability, and I don’t want to spend a Sunday afternoon debugging a pod networking issue when I just want to watch a movie. Proxmox I’ve used before and it’s genuinely good, but it doesn’t solve the idempotency problem. You still end up with VMs and containers that drift over time. Same goes for TrueNAS and similar. I also prefer staying close to the metal. I’m comfortable with Linux and I’d rather own the whole stack than add an abstraction layer I have to understand on top of it.

But if you’re ready to think about your homelab the way a software engineer thinks about code (versioned, tested, reviewable, rollbackable), NixOS is worth every frustrating moment of the learning curve.

See you in Part 2, where we’ll boot the installer and write our first flake.nix.


Further Reading


Written in March 2026. NixOS 25.11 (unstable channel at time of writing), Nix 2.24.