The Problem With Every Other Distro
I ran Ubuntu on my home server for a few years. It worked until it didn't. A do-release-upgrade left me with a broken nginx config, half-migrated Python packages, and a Nextcloud instance that refused to start. The fix took an afternoon. That afternoon made me reconsider the whole model.
The core issue is that traditional Linux systems are imperative and stateful. You get to a working state by running a sequence of commands, and that state is only ever documented if you were disciplined enough to write it down. Most people aren't. I wasn't.
NixOS flips this. You describe the system you want in Nix expressions, and the system converges to that description every time you rebuild. If your config file says nginx is enabled with certain vhost settings, that's exactly what runs. No drift. No "I swear I installed that package last year."
The Declarative Model
Everything that matters lives in /etc/nixos/configuration.nix (or your flake, more on that shortly). Want openssh enabled with key-only auth?
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
};
};
Run nixos-rebuild switch and that's the system. Remove those lines and rebuild: openssh is gone. Not masked, not disabled, gone. The Nix store has the package but the system doesn't reference it and it'll get GC'd eventually.
This matters because the config is the documentation. If I hand this machine to someone else, or rebuild it from scratch, I point at the config repo and the result is the same machine.
Flakes
Flakes pin your entire dependency tree in flake.lock: the nixpkgs revision, any overlays, and any external modules are all locked to specific git commits. Reproducibility becomes concrete rather than aspirational.
A minimal flake.nix for a single host looks like this:
{
description = "homeserver flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
agenix.url = "github:ryantm/agenix";
};
outputs = { self, nixpkgs, agenix, ... }: {
nixosConfigurations.homeserver = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./hosts/homeserver/configuration.nix
agenix.nixosModules.default
];
};
};
}
The flake.lock generated from this pins both nixpkgs and agenix to exact commits. Update with nix flake update, review the diff, commit it. Now your updates are intentional and auditable.
nixpkgs
nixpkgs is the package collection: over 100,000 packages, all defined as Nix derivations that describe inputs, build steps, and outputs deterministically. The same derivation built on two different machines produces bit-for-bit identical output (for most packages).
For a home server, you'll typically track a stable channel like nixos-24.11. If you need something newer from unstable, pull it in selectively via an overlay rather than flipping your whole system to unstable.
Why Reproducibility Matters for a Home Server
The home server context is different from a workstation. Services are long-running, updates are infrequent, and when something breaks it's usually at the worst possible time: you're away, your partner can't access Jellyfin, etc.
With NixOS, rollback is free. If a rebuild breaks something, nixos-rebuild switch --rollback or picking the previous generation from the boot menu gets you back in 30 seconds. Rebuilds from scratch are also straightforward. New hardware? Copy the config, run the installer, rebuild. You're not chasing down 3 years of apt install history.
You can also test changes before committing them. nixos-rebuild test activates the new config without making it the default boot entry. If you're tweaking something risky, verify it works before locking it in.
The mental shift is that you stop thinking about the current state of the machine and start thinking about the desired state in your config. It takes a bit to internalize, but once it clicks you won't want to go back.
Next up: hardware configuration and boot setup.