Raspberry pi
Somewhere under those pins is a reproducible DNS server.

When I decided to build a DNS server on a Raspberry Pi, the obvious choice was Raspbian. It’s what everyone uses. Tutorials everywhere.

But I was already running NixOS on my desktop. Why maintain two different mental models? Why learn Raspbian’s way when I could use the same declarative approach everywhere?

So I went straight to NixOS on the Pi.

What Declarative Gets You

Your Entire System in Config Files

services.unbound = {
  enable = true;
  settings = {
    server = {
      interface = [ "127.0.0.1" "192.168.50.2" ];
      port = 5335;
    };
  };
};

This isn’t a script that runs once. It’s a specification. NixOS reads it and makes reality match. Add a service, rebuild, it’s running. Remove a line, rebuild, it’s gone.

No mystery state. No “I think I installed this but I’m not sure.” If it’s not in the config, it’s not on the system.

Reproducibility

My finite configuration is a git repository. If the SD card dies, I rebuild the exact same system:

nix build .#nixosConfigurations.finite.config.system.build.sdImage

Flash, boot, done. Same packages, same versions, same configuration. No reinstalling. No reconfiguring.

Fearless Experimentation

Want to try a different DNS configuration? Edit the file, rebuild. If it breaks, roll back:

nixos-rebuild switch --rollback

NixOS keeps previous system generations around. The worst case is always “go back to what worked.”

Documentation as Code

The config files are the documentation. What firewall ports are open? Check firewall.nix. How is Unbound configured? Check unbound.nix.

No hunting through /etc wondering if something was modified since install. The Nix files are the source of truth.

The Trade-offs

Learning curve. Nix is a functional language with its own syntax. The first time you see { pkgs, ... }: you’ll wonder what you’ve gotten into. It takes time.

Fewer tutorials. Most Raspberry Pi guides assume Raspbian. You’ll need to translate mentally or search specifically for NixOS solutions.

Slower builds. Building NixOS derivations on a Pi takes forever. I build on my laptop and push the result instead:

nixos-rebuild switch --flake path:.#finite --target-host pi-hole@raspbery_pi_api --sudo --ask-sudo-password

Smaller community. NixOS has devoted users, but it’s not mainstream. You’re off the beaten path.

For a DNS server that I want to run reliably without babysitting, these trade-offs make sense. I’d rather spend time learning Nix than debugging configuration drift.

The Architecture

finite’s configuration is modular:

finite/
├── configuration.nix              # Main entry point
├── hardware-configuration.nix     # Pi bootloader setup
├── modules/
│   ├── unbound.nix               # DNS resolver
│   ├── containers/pi-hole.nix    # Ad blocking
│   ├── network.nix               # Static IP
│   ├── firewall.nix              # Minimal open ports
│   └── ssh.nix                   # Key-based auth
└── settings.nix                  # Your customizations

You touch one file: settings.nix. Everything else just works.

Getting Started

If you want to try NixOS on a Raspberry Pi:

  1. Clone finite. Even if you customize heavily, it’s a working example. github.com/wh1le/finite

  2. Read the NixOS manual. The official docs are dense but comprehensive.

  3. Look at the nixos-hardware repo. github.com/NixOS/nixos-hardware has Pi-specific configurations.

  4. Accept the learning curve. It takes time. The payoff is reproducibility.

For the full story of how I ended up on NixOS, see My NixOS Journey.

Next in this series: The DNS Bootstrap Problem - a fun chicken-and-egg situation where your DNS server needs DNS to start.