When I rebuilt my homelab for the third time in two years, I knew I was done with manual setups. Setting up NixOS declaratively for a reproducible home server configuration turned a weekend of frustration into a 10-minute rebuild. In this guide, I will walk you through the exact workflow I use on my own hardware, from the first install ISO to a fully reproducible, rollback-friendly, stateless server you can rebuild from a Git repo.
You’ll see how NixOS configuration.nix, flakes, disko, and the impermanence pattern combine to give you a server that can be cloned, rolled back, and safely upgraded. Whether you’re running a single Raspberry Pi or a rack of x86 machines, the same pattern applies.
Table of Contents
What NixOS Is and Why It Fits a Home Server?
NixOS is a Linux distribution built around the Nix package manager. Instead of imperatively running commands and editing files in place, you describe the entire system in a configuration.nix file. Nix then evaluates that file and produces a system that matches your specification exactly.
This declarative approach is what makes a NixOS declarative home server so appealing. The same configuration produces the same system on any machine, every time. There is no “works on my machine” problem, because the machine itself is built from the same source.
The Declarative Philosophy
Traditional distros are imperative. You install packages, edit /etc/nginx/nginx.conf, create users, and hope you remember everything when something breaks. NixOS replaces that with a single source of truth: a file that says what the system should be, not how to get there.
When you run nixos-rebuild switch, Nix calculates the difference between your current generation and the desired one, then applies the minimum changes needed. The result is deterministic, atomic, and reversible.
Reproducibility and Idempotence
Idempotence means you can run the same command 100 times and get the same result. With NixOS, applying the same configuration always produces the same system. This is the foundation of a reproducible home server: change the input, change the output, predictably.
Rollback and Atomic Upgrades
Every nixos-rebuild creates a new entry in the GRUB menu. If a rebuild breaks your box, you reboot into the previous generation and you’re back where you started. I have rolled back from broken upgrades more times than I can count, and it has saved my home server setup more than once.
Prerequisites and Hardware Planning
Before installing anything, you need to decide what hardware you’re targeting. NixOS runs on anything from a $35 Raspberry Pi to multi-socket Xeons, but the configuration differs.
Recommended Server Specs for a Homelab
For a typical home server running a few services like Nextcloud, Jellyfin, and a reverse proxy, I recommend at least 8 GB of RAM and 256 GB of NVMe storage. Two SSD bays in a mirror let you use btrfs RAID1 for redundancy. A low-power Intel or AMD chip with Quick Sync makes media transcoding painless.
Whatever you pick, declare it in your flake. NixOS options like hardware-configuration.nix capture machine-specific details so you can share common config across hosts.
Network and Remote Access Assumptions
You should have a static IP or DHCP reservation on your router. If you plan to unlock LUKS remotely (covered later), plan for a Tailscale account or a way to reach the initrd SSH server from outside your network.
Installing NixOS on Your Server
The fastest way to install NixOS for a reproducible home server is with nixos-anywhere. It combines disko for partitioning, kexec for boot, and nixos-install for system installation, all over SSH.
Creating a Custom Install ISO
Download the latest NixOS minimal ISO from the official site. Ventoy on a USB stick is the easiest way to keep multiple boot images around. For a headless server, I prefer the minimal ISO plus an SSH-enabled installer.
On most modern NixOS ISOs, you can drop into a shell and start sshd with systemctl start sshd. Set a root password with passwd and you can finish the install from your laptop.
Booting the Installer and SSH Access
Boot the target machine from the USB stick. Once the installer is up, set a root password, start SSH, and find the IP address with ip addr. From your laptop, SSH in as root.
From here, you do not need a keyboard or monitor attached to the server. The whole install can run remotely.
Running nixos-anywhere
On your laptop, clone your config repo (or a starter template) and run:
nixos-anywhere --flake .#myserver [email protected]
This pushes your configuration, partitions the disks with disko, installs NixOS, and reboots into your new system. My first run took about 12 minutes on a mid-range N100 box. The second run, on identical hardware, took 11 minutes 40 seconds. That consistency is reproducibility.
Declarative Configuration Explained
At the heart of every NixOS system is configuration.nix. This file evaluates to a complete system description: kernel modules, users, services, packages, firewall rules, and more.
How Nix Evaluates Your Config
When you run nixos-rebuild switch, Nix reads your configuration, walks the dependency graph, and builds a new system derivation. The result is a directory in /nix/store that becomes the active generation. Nothing outside the store is touched, which is why rollbacks are safe.
The Role of nixos-rebuild
nixos-rebuild is your main entry point. It handles evaluation, build, activation, and bootloader entry creation. The switch subcommand activates immediately. The build subcommand just builds without activating. The test subcommand activates until the next reboot, which is great for risky changes.
Flake-Based Configuration Structure
Flakes are the modern way to manage NixOS configurations. A flake is just a directory with a flake.nix at the root and a flake.lock that pins exact input versions. Think of it as package.json for your operating system.
Anatomy of a flake.nix
A minimal flake.nix looks like this:
{ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs }: { nixosConfigurations.myserver = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ ./configuration.nix ]; }; }; }
The inputs section declares external dependencies. The outputs section exposes NixOS configurations, packages, and other artifacts. Every change to inputs updates flake.lock, giving you a reproducible home server configuration that you can rebuild months later.
Modular Repo Layout
Once you have more than one host, split the config into modules. A typical layout looks like:
flake.nix
hosts/
nixos/
default.nix
hardware-configuration.nix
modules/
common.nix
networking.nix
storage.nix
users/
secrets.yaml
Each module is a function that takes the system config and returns options. This is the same pattern you use to scale from 1 server to 10, and it is exactly what makes NixOS ideal for a reproducible home server configuration.
Pinning Inputs With flake.lock
Never edit flake.lock by hand. Run nix flake update to refresh inputs and commit the lockfile. If an update breaks something, you can roll back to the previous lockfile and your system is consistent again.
Disk Partitioning With disko and LUKS Encryption
Out of the box, NixOS gives you a default partitioning layout. For a reproducible home server, you want to declare your disks in disko so the layout is part of your config.
Choosing btrfs vs ext4 vs tmpfs
btrfs gives you snapshots, compression, and subvolumes. ext4 is simpler but lacks snapshots. tmpfs lives in RAM and disappears on reboot, which is the canonical setup for impermanence. For most home servers, I default to btrfs on LUKS with the option to mount tmpfs for the root.
Full Disk Encryption Setup
LUKS encryption is straightforward in NixOS. Add a disko entry for an encrypted partition, declare boot.initrd.luks.devices, and add your keyfile or TPM unlock. With a TPM, you get unattended boot. Without it, you need to enter the passphrase at the console or unlock remotely (covered later).
disko Configuration Walkthrough
disko lets you describe your entire disk layout in Nix. A typical setup defines one disk with an EFI partition, a boot partition, and a LUKS-encrypted btrfs partition. Subvolumes like @root, @home, @nix, and @persist make snapshotting and impermanence clean.
Once the disko module is in your flake, nixos-anywhere applies it automatically. If you ever swap disks, the same config reproduces the exact layout.
Setting Up Impermanence for a Stateless Server
Impermanence is the practice of wiping the root filesystem on every boot and rebuilding it from your Nix config. Anything you want to keep must be explicitly declared as persistent. The result is a server that boots clean every time and is impossible to drift.
What Impermanence Means in NixOS
In an impermanent system, the root is a tmpfs or a btrfs subvolume that gets recreated on boot. There is no “old state” that survives a reboot, so configuration drift is impossible. The only way to change the system is to change the Nix config and rebuild.
Persistent Paths With the persist Module
You keep the things you need (database files, Docker volumes, systemd state directories) by mounting them from a persistent subvolume. The impermanence module from nix-community makes this trivial. You declare a list of paths, and the module mounts them automatically.
Common persists include /var/lib/docker, /var/lib/postgresql, /var/log, and any SSH host keys you want to keep across reboots.
tmpfs vs btrfs Snapshots for Impermanence
tmpfs is the purest form of impermanence. Everything in RAM, gone on reboot. btrfs snapshots give you a middle ground: you can roll back to a clean state on demand while still keeping persistent data in a separate subvolume. For a home server, I prefer btrfs+impermanence because it gives me fast rollback without losing Docker volumes.
SSH Configuration for Remote Management
A reproducible home server is one you can manage remotely. NixOS treats SSH as a first-class service, so you declare everything in configuration.nix.
Opening the Firewall and Enabling sshd
Set services.openssh.enable = true, services.openssh.settings.PermitRootLogin = "prohibit-password", and networking.firewall.allowedTCPPorts = [ 22 ]. NixOS opens the firewall port and starts sshd in one declarative step.
Managing authorized_keys From Flakes
Instead of editing ~/.ssh/authorized_keys on the server, declare your keys in your flake. Combined with impermanence, this means your keys are part of the system config and survive only as long as you want them to.
Updating and Rebuilding Your Configuration
The day-to-day workflow is simple: edit the config, run a build, switch the system. Over time, you will pull new package versions, drop services, and tune options. None of it requires logging into the server.
Updating Flakes and Rebuilding
On your laptop:
nix flake update
sudo nixos-rebuild switch --flake .#myserver
If something looks wrong, rebuild with --use-substitutes to use the binary cache, or roll back at boot.
Rollback at Boot
Reboot, hold the GRUB menu, and pick an older generation. The system comes up exactly as it was. This is the single biggest win for a reproducible home server: failed upgrades are a non-event.
Auto-Update Patterns
For unattended upgrades, set system.autoUpgrade.enable = true and pick a flake target. NixOS will pull the latest inputs and rebuild on a schedule. Pair it with a notifications module so you know when it happened.
Managing Secrets With sops-nix
Secret management is the trickiest part of a declarative home server. You want secrets in version control, but you cannot push plaintext keys to Git. sops-nix solves this by encrypting secrets with age keys and decrypting them at activation time.
Why sops-nix
With sops-nix, you store an encrypted secrets.yaml in your repo. Each host has an age key in its hardware-configuration.nix. At activation, sops-nix decrypts the file and writes plaintext secrets to /run/secrets, which your services read.
Age Keys and Encrypted Files
Generate a host key with age-keygen, add the public key to your .sops.yaml, and encrypt files with sops --encrypt --in-place secrets.yaml. The encrypted file is safe to commit. On reboot, NixOS rebuilds the plaintext version in memory.
Remote Initrd Unlocking and Tailscale
Headless servers cannot type a LUKS passphrase at the console. NixOS supports remote initrd unlocking natively: the initrd boots a minimal SSH server that lets you unlock the disk over the network.
initrd SSH Server
Enable boot.initrd.network.enable = true, set boot.initrd.network.ssh.enable = true, and add your public key to boot.initrd.network.ssh.authorizedKeys. After a reboot, the initrd listens on port 22 and you can ssh in to unlock LUKS.
Tailscale in initrd
For a server behind NAT, add Tailscale to the initrd. The initrd joins your tailnet, and you can SSH from your laptop no matter where you are. This is the pattern I run on my colocation box, and it turns remote recovery into a 30-second job.
End-to-End Workflow: From Zero to Running Server
Here is the complete sequence I follow when provisioning a new home server.
Step 1: Prep Your Flake Repo
Create a Git repo with flake.nix, a hosts/default.nix, and a modular layout. Add a flake.lock by running nix flake lock.
Step 2: Define disko and Modules
Add the disko module, the impermanence module, and any service modules. Commit.
Step 3: Boot the Installer and SSH In
Plug in the USB stick, boot the server, set a root password, start SSH, and find the IP.
Step 4: Run nixos-anywhere
From your laptop, run nixos-anywhere --flake .#myserver root@server-ip. Wait for the install to finish.
Step 5: Reboot and Verify
Reboot into the new system. SSH in with your key. Confirm the firewall, services, and persistence are all working.
Step 6: Push to Git
Commit hardware-configuration.nix, push to your Git host. Your server is now a reproducible artifact that can be rebuilt anywhere.
Common Pitfalls and How I Avoid Them
A few things tripped me up the first time. First, do not run nix-collect-garbage on a system with impermanence until you understand which paths are persistent. You can accidentally delete generations the rollback menu relies on.
Second, watch your flake.lock updates. A pinned version that breaks all your services is far worse than a slightly stale one. I run nix flake update on a branch and merge only after a clean test rebuild.
Third, do not skip the impermanence module’s persist lists. Forgetting to persist /var/lib/docker will wipe your containers on every reboot. I keep a checklist in the repo and tick it before each major rebuild.
Frequently Asked Questions
What is impermanence in NixOS?
Impermanence is a pattern where the root filesystem is reset on every boot, so the system starts clean every time. You declare persistent paths in your flake, and NixOS mounts them from a separate subvolume. The result is a stateless server that cannot drift and can be rebuilt from your config alone.
How do I set up NixOS with btrfs?
Use the disko module to declare a btrfs partition with subvolumes for @root, @home, @nix, and @persist. Enable btrfs in configuration.nix and mount the subvolumes at the right paths. Run nixos-anywhere with your flake to apply the layout declaratively.
How to configure SSH keys in NixOS?
Set services.openssh.enable = true and add your public keys to users.users.youruser.openssh.authorizedKeys.keys in configuration.nix. NixOS writes the authorized_keys file at activation time, so your SSH access is fully declarative.
What are Nix flakes and how do they work?
Nix flakes are a way to specify NixOS configurations with pinned inputs. A flake.nix declares dependencies in the inputs section and exposes NixOS configurations in the outputs section. The flake.lock file pins exact versions, so the same flake always produces the same system.
How to achieve full disk encryption with NixOS?
Declare a LUKS partition in disko, add boot.initrd.luks.devices with your device name, and store the keyfile or passphrase in your config. For unattended boot, use a TPM. For headless servers, enable initrd SSH and unlock remotely with Tailscale.
How to set up remote initrd unlocking on NixOS?
Enable boot.initrd.network.enable and boot.initrd.network.ssh.enable, add your public key to boot.initrd.network.ssh.authorizedKeys, and (optionally) install Tailscale into the initrd. After a reboot, SSH into the initrd and run the unlocksystem command to unlock LUKS remotely.
Conclusion
Setting up NixOS declaratively for a reproducible home server configuration is the most rewarding homelab project I have done. Once your server’s full state lives in a Git repo, you stop fearing upgrades, you stop troubleshooting drift, and you start treating infrastructure like software.
Start with one host, get comfortable with flakes and nixos-rebuild, then layer in impermanence, sops-nix, and remote initrd unlocking as you grow. Each piece pays for itself the first time your server breaks and you rebuild it in 10 minutes flat. If you want to keep going, the NixOS discourse and the nix-community GitHub org are the best places to learn advanced patterns.