<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Systems on Manuel Herrmann</title>
    <link>https://blog.0x17.de/categories/systems/</link>
    <description>Recent content in Systems on Manuel Herrmann</description>
    <image>
      <title>Manuel Herrmann</title>
      <url>https://blog.0x17.de/images/mh.jpg</url>
      <link>https://blog.0x17.de/images/mh.jpg</link>
    </image>
    <generator>Hugo -- 0.159.0</generator>
    <language>en</language>
    <lastBuildDate>Sat, 21 Mar 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog.0x17.de/categories/systems/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>One Git Repo to Rule Them All: Managing 12 Machines with NixOS</title>
      <link>https://blog.0x17.de/post/managing-12-machines-with-nixos/</link>
      <pubDate>Sat, 21 Mar 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.0x17.de/post/managing-12-machines-with-nixos/</guid>
      <description>How I went from manually babysitting Linux boxes to declaratively managing servers, desktops, a gaming rig, and a VR machine from a single git repo - and why I&amp;#39;ll never go back.</description>
      <content:encoded><![CDATA[<h2 id="the-pain-before-the-solution">The Pain Before the Solution</h2>
<p>I started with Gentoo. If you know, you know: USE flags, <code>emerge --sync</code>, watching a browser compile for six hours, and the quiet satisfaction of a system shaped entirely to your will. It was maximum configuration at maximum cost - every package exactly how you wanted it, every kernel option hand-chosen, every dependency chain personally audited. It was also completely unreproducible. The system on disk was the result of hundreds of commands typed across months. If the disk died, so did the configuration. You could document it, but documentation and reality drift apart fast.</p>
<p><img alt="nixos journey" loading="lazy" src="/post/managing-12-machines-with-nixos/nixos-journey.png"></p>
<p>After a few years of that, I migrated my servers to Debian. Less customization, fewer surprises, faster iteration. Debian is rock solid. But the gap between &ldquo;I&rsquo;ve configured this&rdquo; and &ldquo;this is reproducible&rdquo; never closed. The configuration lived partly in files, partly in database state, partly in my memory. Upgrading a server meant hoping nothing drift-collided with the new packages. I learned to fear dist-upgrades.</p>
<p>Multi-machine management made this worse. When I had three servers to keep consistent, I reached for Ansible. Later tried Salt. Both are genuinely useful tools - they brought the configuration into version control, made state more explicit, handled idempotent runs reasonably well. But they&rsquo;re scaffolding over a fundamentally mutable OS. Ansible describes <em>actions</em> to take, not the <em>state</em> to achieve. The playbook says &ldquo;ensure this package is installed&rdquo; and &ldquo;write this file,&rdquo; but it says nothing about what happens to the thousand other files the system has accumulated over the years. You run the playbook, the system mostly matches what you expect, and you&rsquo;re left with a lingering uncertainty: what else is in there?</p>
<p>Building a WireGuard mesh with Salt illustrates the problem. You write YAML templates, manage state files, think carefully about idempotency edge cases, and you still have hosts where salt-minion mysteriously fell out of sync six months ago and nobody noticed. The tool is fighting the problem. You&rsquo;re layering abstraction on top of a mutable foundation and hoping the accretion doesn&rsquo;t bite you.</p>
<p>NixOS is different in kind, not just in degree. The whole system is declared as a pure function over text files. Run the same config on two machines and you get the same system. Roll back to yesterday&rsquo;s config and you get yesterday&rsquo;s system. The &ldquo;what else is in there&rdquo; question dissolves because the system is a direct consequence of what&rsquo;s in the repo.</p>
<p>That framing clicked for me when I started this config, now covering a dozen machines across servers, desktops, a gaming rig, and a VR setup. This isn&rsquo;t a single desktop experiment. It&rsquo;s a homelab, and the whole thing lives in one git repository.</p>
<hr>
<h2 id="what-nixos-actually-is">What NixOS Actually Is</h2>
<p>The central insight: your entire NixOS system is a pure function. Given the same inputs - configuration files, package hashes, module options - you get the same, bit-for-bit reproducible system. Not &ldquo;mostly the same.&rdquo; The same.</p>
<p>Three consequences fall out of this:</p>
<p><strong>Reproducibility.</strong> If a coworker asks for the same toolchain you use, you share a Nix expression instead of a wiki page. It works the first time.</p>
<p><strong>Rollback.</strong> Every <code>nixos-rebuild switch</code> creates a new system generation. If the new kernel panics at boot, you select the previous generation from the bootloader menu. The old generation is still there on disk, unchanged. You lose nothing.</p>
<p><strong>Fearless experimentation.</strong> Want to try a different init setup, a new compositor, a service you&rsquo;ve never configured before? Add it, switch, try it. If it&rsquo;s wrong, roll back. The cost of experimentation drops to nearly zero.</p>
<p>It&rsquo;s worth being clear about what NixOS is <em>not</em>. It&rsquo;s not just a package manager (though it has one). It&rsquo;s not Docker - there are no containers or images involved in basic NixOS usage, and your system packages live in the Nix store, not a layered filesystem. It&rsquo;s not Ansible - there are no playbooks, no target hosts, no SSH connections during a normal rebuild. The system is <em>built</em> from a description; it doesn&rsquo;t <em>describe operations</em> to run against a running system.</p>
<p>The honest note on the learning curve: Nix the language is genuinely strange. It&rsquo;s lazy and functional, variables are declarations rather than assignments, and error messages can be opaque. The first two weeks feel like reading documentation written for people who already understand it. That phase ends, and what&rsquo;s on the other side is worth it. But it is a real ramp.</p>
<hr>
<h2 id="the-flake-one-entry-point-for-everything">The Flake: One Entry Point for Everything</h2>
<p>A flake is Nix&rsquo;s mechanism for pinned, reproducible builds. A <code>flake.nix</code> declares what your project depends on (inputs) and what it produces (outputs). The <code>flake.lock</code> file pins every input to an exact commit hash and content hash, giving you a complete bill of materials for your infrastructure.</p>
<p>The inputs section of this config shows the pinning strategy clearly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>inputs <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  nixpkgs<span style="color:#f92672">.</span>url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:nixos/nixpkgs?ref=nixos-unstable&#34;</span>;
</span></span><span style="display:flex;"><span>  home-manager <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:nix-community/home-manager&#34;</span>;
</span></span><span style="display:flex;"><span>    inputs<span style="color:#f92672">.</span>nixpkgs<span style="color:#f92672">.</span>follows <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  sops <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:Mic92/sops-nix&#34;</span>;
</span></span><span style="display:flex;"><span>    inputs<span style="color:#f92672">.</span>nixpkgs<span style="color:#f92672">.</span>follows <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  disko <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:nix-community/disko&#34;</span>;
</span></span><span style="display:flex;"><span>    inputs<span style="color:#f92672">.</span>nixpkgs<span style="color:#f92672">.</span>follows <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  hyprland <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:hyprwm/Hyprland/v0.54.2&#34;</span>;
</span></span><span style="display:flex;"><span>    inputs<span style="color:#f92672">.</span>nixpkgs<span style="color:#f92672">.</span>follows <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  hy3 <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:outfoxxed/hy3?ref=hl0.54.2&#34;</span>;
</span></span><span style="display:flex;"><span>    inputs<span style="color:#f92672">.</span>hyprland<span style="color:#f92672">.</span>follows <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;hyprland&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  lanzaboote <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    url <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;github:nix-community/lanzaboote&#34;</span>;
</span></span><span style="display:flex;"><span>    inputs<span style="color:#f92672">.</span>nixpkgs<span style="color:#f92672">.</span>follows <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nixpkgs&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Notice two things: <code>inputs.nixpkgs.follows = &quot;nixpkgs&quot;</code> propagates throughout, ensuring all these tools share one nixpkgs version rather than each pulling their own. And Hyprland is pinned to <code>v0.54.2</code> with hy3 at the matching <code>hl0.54.2</code> ref - intentional, because the Hyprland plugin API changes between releases.</p>
<p>For that last point, pinning isn&rsquo;t enough if you need the pinned version available system-wide. The overlay pattern solves it. An overlay in Nix is a function that takes the final and previous package sets and returns overrides. The hyprland overlay replaces the nixpkgs hyprland package with the pinned one:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>hyprlandOverlay <span style="color:#f92672">=</span> final: prev:
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">let</span>
</span></span><span style="display:flex;"><span>    hyprlandPkgs <span style="color:#f92672">=</span> inputs<span style="color:#f92672">.</span>hyprland<span style="color:#f92672">.</span>packages<span style="color:#f92672">.</span><span style="color:#e6db74">${</span>system<span style="color:#e6db74">}</span>;
</span></span><span style="display:flex;"><span>    hy3Pkgs <span style="color:#f92672">=</span> inputs<span style="color:#f92672">.</span>hy3<span style="color:#f92672">.</span>packages<span style="color:#f92672">.</span><span style="color:#e6db74">${</span>system<span style="color:#e6db74">}</span>;
</span></span><span style="display:flex;"><span>    hyprlandPkg <span style="color:#f92672">=</span> hyprlandPkgs<span style="color:#f92672">.</span>hyprland or hyprlandPkgs<span style="color:#f92672">.</span>default;
</span></span><span style="display:flex;"><span>    hy3Pkg <span style="color:#f92672">=</span> hy3Pkgs<span style="color:#f92672">.</span>hy3 or hy3Pkgs<span style="color:#f92672">.</span>default;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    hyprland <span style="color:#f92672">=</span> hyprlandPkg;
</span></span><span style="display:flex;"><span>    hyprland-unwrapped <span style="color:#f92672">=</span> hyprlandPkgs<span style="color:#f92672">.</span>hyprland-unwrapped or hyprlandPkg;
</span></span><span style="display:flex;"><span>    hyprlandPackages <span style="color:#f92672">=</span> hyprlandPkgs;
</span></span><span style="display:flex;"><span>    hyprlandPlugins <span style="color:#f92672">=</span> (prev<span style="color:#f92672">.</span>hyprlandPlugins or { }) <span style="color:#f92672">//</span> { hy3 <span style="color:#f92672">=</span> hy3Pkg; };
</span></span><span style="display:flex;"><span>    hy3 <span style="color:#f92672">=</span> hy3Pkg;
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p>This overlay gets applied in <code>nixpkgs.overlays</code>, so every module in the entire config that references <code>pkgs.hyprland</code> gets the pinned version. No special imports, no passing versions around - the whole system agrees on one build.</p>
<p>The most satisfying part of the flake setup is how new hosts appear automatically. There&rsquo;s no host registration, no list to update, no imports to add:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>hostDirs <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>attrNames (lib<span style="color:#f92672">.</span>filterAttrs (_: v: v <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;directory&#34;</span>) (builtins<span style="color:#f92672">.</span>readDir <span style="color:#e6db74">./hosts</span>));
</span></span><span style="display:flex;"><span>mkHosts <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>genAttrs hostDirs;
</span></span></code></pre></div><p><code>builtins.readDir ./hosts</code> returns an attrset mapping names to types (<code>&quot;directory&quot;</code>, <code>&quot;regular&quot;</code>, etc.). Filter to directories, take the names, and generate a NixOS configuration for each one. Every directory inside <code>hosts/</code> automatically becomes a machine. Add a folder, add a <code>default.nix</code> inside it, run <code>nixos-rebuild</code> or deploy - that&rsquo;s it.</p>
<hr>
<h2 id="roles-one-module-system-for-12-different-machines">Roles: One Module System for 12 Different Machines</h2>
<p>Twelve machines sound like twelve configs to maintain. They&rsquo;re not. Machines are variations on a theme, and the theme is expressed as roles.</p>
<p>The role system starts with a typed option in <code>modules/default.nix</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>options<span style="color:#f92672">.</span>ox<span style="color:#f92672">.</span>roles <span style="color:#f92672">=</span> mkOption {
</span></span><span style="display:flex;"><span>  default <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;desktop&#34;</span> ];
</span></span><span style="display:flex;"><span>  type <span style="color:#f92672">=</span> <span style="color:#66d9ef">with</span> lib<span style="color:#f92672">.</span>types; listOf (enum [ <span style="color:#e6db74">&#34;desktop&#34;</span> <span style="color:#e6db74">&#34;laptop&#34;</span> <span style="color:#e6db74">&#34;server&#34;</span> <span style="color:#e6db74">&#34;games&#34;</span> <span style="color:#e6db74">&#34;vr&#34;</span> <span style="color:#e6db74">&#34;nas&#34;</span> ]);
</span></span><span style="display:flex;"><span>  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    The machine roles.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>options<span style="color:#f92672">.</span>ox<span style="color:#f92672">.</span>role <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>  lib<span style="color:#f92672">.</span>genAttrs [ <span style="color:#e6db74">&#34;desktop&#34;</span> <span style="color:#e6db74">&#34;laptop&#34;</span> <span style="color:#e6db74">&#34;server&#34;</span> <span style="color:#e6db74">&#34;games&#34;</span> <span style="color:#e6db74">&#34;vr&#34;</span> <span style="color:#e6db74">&#34;nas&#34;</span> ] (name:
</span></span><span style="display:flex;"><span>    mkOption {
</span></span><span style="display:flex;"><span>      type <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>types<span style="color:#f92672">.</span>bool;
</span></span><span style="display:flex;"><span>      readOnly <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>      default <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>elem name config<span style="color:#f92672">.</span>ox<span style="color:#f92672">.</span>roles;
</span></span><span style="display:flex;"><span>      description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Whether this machine has the </span><span style="color:#e6db74">${</span>name<span style="color:#e6db74">}</span><span style="color:#e6db74"> role.&#34;</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  ) <span style="color:#f92672">//</span> {
</span></span><span style="display:flex;"><span>    enduser <span style="color:#f92672">=</span> mkOption {
</span></span><span style="display:flex;"><span>      type <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>types<span style="color:#f92672">.</span>bool;
</span></span><span style="display:flex;"><span>      readOnly <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>      default <span style="color:#f92672">=</span> config<span style="color:#f92672">.</span>ox<span style="color:#f92672">.</span>role<span style="color:#f92672">.</span>desktop <span style="color:#f92672">||</span> config<span style="color:#f92672">.</span>ox<span style="color:#f92672">.</span>role<span style="color:#f92672">.</span>laptop;
</span></span><span style="display:flex;"><span>      description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Whether this machine has a desktop or laptop role.&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span></code></pre></div><p><code>ox.roles</code> is a list you set per-host: <code>[&quot;server&quot;]</code>, <code>[&quot;desktop&quot; &quot;games&quot; &quot;vr&quot;]</code>, whatever the machine is. <code>ox.role</code> derives boolean attributes from that list automatically - <code>role.server</code>, <code>role.desktop</code>, and so on - plus the computed <code>role.enduser</code> which is true for any machine a human sits in front of.</p>
<p>Those booleans then drive defaults for every feature in the system:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>ox <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  auth<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  boot<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  samba<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>enduser;
</span></span><span style="display:flex;"><span>  bluetooth<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>laptop;
</span></span><span style="display:flex;"><span>  printing<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>enduser;
</span></span><span style="display:flex;"><span>  gui<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>enduser;
</span></span><span style="display:flex;"><span>  k8s<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault (role<span style="color:#f92672">.</span>desktop <span style="color:#f92672">||</span> role<span style="color:#f92672">.</span>laptop <span style="color:#f92672">||</span> role<span style="color:#f92672">.</span>server);
</span></span><span style="display:flex;"><span>  k8s<span style="color:#f92672">.</span>server<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>server;
</span></span><span style="display:flex;"><span>  ffmpeg<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>enduser;
</span></span><span style="display:flex;"><span>  hyprland<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>enduser;
</span></span><span style="display:flex;"><span>  packages<span style="color:#f92672">.</span>nixEnable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault role<span style="color:#f92672">.</span>enduser;
</span></span><span style="display:flex;"><span>  ssh<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkDefault <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>When a machine is a server, <code>gui.enable</code>, <code>hyprland.enable</code>, <code>bluetooth.enable</code>, <code>printing.enable</code> all default to false. When it&rsquo;s a laptop, bluetooth turns on. When it&rsquo;s an enduser machine, you get Hyprland, Samba mounts, printing, and the Nix development packages. All without writing those conditions in twelve separate host configs. <code>mkDefault</code> means any individual host can still override a specific option - the defaults are overridable, not forced.</p>
<p>Look at the full <code>brunnen</code> host config, a production server:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>ox <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  hostName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;brunnen&#34;</span>;
</span></span><span style="display:flex;"><span>  boot <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;grub&#34;</span>;
</span></span><span style="display:flex;"><span>    efi<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">false</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  roles <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;server&#34;</span> ];
</span></span><span style="display:flex;"><span>  networking<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>That&rsquo;s it. Eight lines of meaningful configuration plus boilerplate imports. The role system handles the rest - no GUI, no Bluetooth, no printing, no Hyprland, no gaming tools. Clean.</p>
<p>Now look at <code>itw</code>, the gaming desktop:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>ox <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  hostName <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;itw&#34;</span>;
</span></span><span style="display:flex;"><span>  boot <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    efi<span style="color:#f92672">.</span>dir <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/boot/EFI&#34;</span>;
</span></span><span style="display:flex;"><span>    cryptPart <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;crypt&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  roles <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;desktop&#34;</span> <span style="color:#e6db74">&#34;games&#34;</span> <span style="color:#e6db74">&#34;vr&#34;</span> ];
</span></span><span style="display:flex;"><span>  monitor<span style="color:#f92672">.</span>with4k <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  monitor<span style="color:#f92672">.</span>with4kFix <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  video <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nvidia&#34;</span>;
</span></span><span style="display:flex;"><span>  wayland<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>environment<span style="color:#f92672">.</span>etc<span style="color:#f92672">.</span>crypttab <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  text <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    games /dev/disk/by-uuid/0292b609-...  /etc/luks-keys/games.key luks
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>fileSystems <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;/games&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    device <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/dev/mapper/games&#34;</span>;
</span></span><span style="display:flex;"><span>    fsType <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ext4&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;/mnt/win&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    device <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/dev/disk/by-partuuid/3d69d556-...&#34;</span>;
</span></span><span style="display:flex;"><span>    fsType <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;ntfs&#34;</span>;
</span></span><span style="display:flex;"><span>    options <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;defaults&#34;</span> <span style="color:#e6db74">&#34;noauto&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>This is the most complex host in the repo. NVIDIA GPU, 4K display with DPI fixes, encrypted <code>/games</code> partition with a keyfile, dual-boot Windows on an NTFS mount, VR role on top of gaming. And the entire description still fits on a screen. The role system handles enabling Steam, Lutris, Gamescope, and related tools via <code>role.games</code>; the VR tools via <code>role.vr</code>; the Hyprland Wayland compositor via <code>role.enduser</code>. Setting <code>video = &quot;nvidia&quot;</code> injects the correct Wayland environment variables automatically - more on that in a moment.</p>
<p>That&rsquo;s the full range of the fleet: from a minimal server to a maxed-out gaming and VR workstation, with nothing special needed in between.</p>
<hr>
<h2 id="home-manager-your-dotfiles-are-config-too">home-manager: Your Dotfiles Are Config Too</h2>
<p>NixOS manages the system - packages, services, kernel configuration, networking. home-manager extends the same declarative model into the user&rsquo;s home directory. It manages dotfiles, user services, user packages, and shell configuration, all in the same language and the same git repo.</p>
<p>The <code>modules/home.nix</code> bridges the NixOS config and home-manager with one critical pattern: <code>gconfig</code>. When home-manager modules are evaluated, they need access to the top-level NixOS configuration to ask role questions. <code>gconfig</code> is passed as an extra special argument:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>home-manager<span style="color:#f92672">.</span>extraSpecialArgs <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  gconfig <span style="color:#f92672">=</span> config;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">inherit</span> oxpkgs;
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Now any home-manager module can ask <code>gconfig.ox.role.enduser</code> or <code>gconfig.ox.wayland.enable</code> and get the host-level answer. The <code>home.nix</code> uses this immediately:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>ox<span style="color:#f92672">.</span>home<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> mkDefault config<span style="color:#f92672">.</span>ox<span style="color:#f92672">.</span>role<span style="color:#f92672">.</span>enduser;
</span></span></code></pre></div><p>Servers don&rsquo;t get a user home-manager setup at all. Desktops and laptops do.</p>
<p>The Emacs configuration is the best example of what this buys you. In a single declaration:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>home <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  packages <span style="color:#f92672">=</span> [ emacs emacsedit ];
</span></span><span style="display:flex;"><span>  file<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;.emacs&#34;</span><span style="color:#f92672">.</span>source <span style="color:#f92672">=</span> <span style="color:#e6db74">./dot_emacs</span>;
</span></span><span style="display:flex;"><span>  activation<span style="color:#f92672">.</span>emacs-config <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>hm<span style="color:#f92672">.</span>dag<span style="color:#f92672">.</span>entryAfter [ <span style="color:#e6db74">&#34;writeBoundary&#34;</span> ] <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    [ -d &#34;$HOME/git&#34; ] || run mkdir -p &#34;$HOME/git&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    [ -d &#34;$HOME/git/emacs-config&#34; ] || run </span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>git<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/git clone \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      https://github.com/0x17de/emacs-config &#34;$HOME/git/emacs-config&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    [ -f &#34;$HOME/git/emacs-config/custom.el&#34; ] || run touch \
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#34;$HOME/git/emacs-config/custom.el&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">  &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>systemd<span style="color:#f92672">.</span>user<span style="color:#f92672">.</span>services<span style="color:#f92672">.</span>emacs-daemon <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  Unit <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    Description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;Emacs Daemon&#34;</span>;
</span></span><span style="display:flex;"><span>    After <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;graphical-session.target&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  Service <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    Type <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;notify&#34;</span>;
</span></span><span style="display:flex;"><span>    ExecStart <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>emacs-pgtk<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/emacs --fg-daemon&#34;</span>;
</span></span><span style="display:flex;"><span>    ExecStop <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>pkgs<span style="color:#f92672">.</span>emacs-pgtk<span style="color:#e6db74">}</span><span style="color:#e6db74">/bin/emacsclient --eval &#39;(kill-emacs)&#39;&#34;</span>;
</span></span><span style="display:flex;"><span>    Restart <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;on-failure&#34;</span>;
</span></span><span style="display:flex;"><span>    RestartSec <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;2s&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  Install <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    WantedBy <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;graphical-session.target&#34;</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>Four things at once: installs <code>emacs-pgtk</code> and wrapper scripts (<code>emacs</code> and <code>emacsedit</code>), writes a <code>.emacs</code> bootstrap file, registers a <code>systemd</code> user service that starts the daemon after the graphical session is up, and clones the external Emacs configuration repository on first activation. Fresh machine, run the rebuild, open Emacs - everything is there.</p>
<p>Where to draw the line: Nix manages the install, the service lifecycle, and the initial bootstrap. The Emacs config itself (keybindings, packages, themes) lives in its own git repository and manages itself through <code>package.el</code> or <code>use-package</code>. Nix doesn&rsquo;t try to own what the application is good at managing.</p>
<hr>
<h2 id="the-desktop-stack">The Desktop Stack</h2>
<h3 id="hyprland-and-the-overlay-pattern">Hyprland and the Overlay Pattern</h3>
<p>Hyprland&rsquo;s Wayland compositor is pinned to v0.54.2 across the whole fleet. This is intentional - the plugin API changes with releases, and the hy3 tiling plugin must match. The overlay from Section 3 means every machine that runs Hyprland gets the same version without any per-host specification.</p>
<p>Monitor configuration is generated per-host from <code>ox.hyprland.monitors</code>. The <code>itw</code> host sets <code>monitor.with4k = true</code> and <code>monitor.with4kFix = true</code>, which triggers the correct scaling and DPI configuration in the Hyprland module. When <code>ox.video = &quot;nvidia&quot;</code>, the module automatically injects the Wayland-specific NVIDIA environment variables (<code>GBM_BACKEND</code>, <code>__GLX_VENDOR_LIBRARY_NAME</code>, <code>LIBVA_DRIVER_NAME</code>, and the rest). No hunting through Arch Wiki pages at 2am for the magic env var combination. Declare the GPU, get the right environment.</p>
<h3 id="rebuilding-the-running-system">Rebuilding the Running System</h3>
<p>The most-used command in the workflow is <code>nho</code>, defined in <code>modules/nushell/config/nix.nu</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-fallback" data-lang="fallback"><span style="display:flex;"><span>def nho --wrapped [...rest] {
</span></span><span style="display:flex;"><span>    cd ~/git/nix
</span></span><span style="display:flex;"><span>    nh os switch . ...$rest
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><code>nh</code> is a wrapper around <code>nixos-rebuild</code> that adds better output formatting, diff reporting, and some quality-of-life features. This alias changes into the repo directory and invokes <code>nh os switch</code> - rebuild from the current flake, apply immediately. The <code>--wrapped</code> flag in Nushell passes extra arguments through unchanged, so <code>nho --hostname other-machine</code> works for remote deploys.</p>
<p>Make a config change, type <code>nho</code>, the system rebuilds and switches atomically. If something is wrong, the previous generation is one bootloader selection away. That feedback loop changes how you interact with your configuration.</p>
<h3 id="nushell-as-primary-shell">Nushell as Primary Shell</h3>
<p>Nushell runs as the primary interactive shell on all enduser machines. Its typed pipeline model, structured data handling, and built-in table rendering make it particularly good for homelab work. The config includes a suite of plugins: <code>polars</code> for dataframe operations, <code>query</code> for SQL-like filtering, <code>net</code> for network diagnostics, <code>formats</code> for parsing structured file formats. These are declared in the nushell module and activated automatically.</p>
<h3 id="gaming-on-nixos">Gaming on NixOS</h3>
<p>NixOS has a reputation for being difficult for gaming. That reputation is stale.</p>
<p>Steam, Lutris, Heroic Games Launcher, ProtonUp-Qt, MangoHUD, and Gamescope are all declared in the <code>games</code> module, enabled automatically when <code>role.games</code> is true. Steam&rsquo;s FHS compatibility layer handles most Windows titles without any special configuration. Proton versions are managed via ProtonUp-Qt.</p>
<p>The occasional game that absolutely requires an FHS environment - a proprietary launcher that hardcodes library paths, for instance - can be wrapped in a custom <code>buildFHSEnv</code> expression. That&rsquo;s about ten lines of Nix and not a deep rabbit hole. <code>nix-alien</code> was tried and found unreliable; the <code>buildFHSEnv</code> approach is more transparent and predictable.</p>
<p>NVIDIA gaming is handled by the <code>video = &quot;nvidia&quot;</code> flag described above. On <code>itw</code>, Gamescope handles frame pacing and fullscreen correctly for both native Linux titles and Windows games via Proton.</p>
<p>The role system means gaming tools are only present on machines that declare <code>role.games</code>. Servers stay lean. Desktops used for work but not gaming skip the Steam runtime and all the associated tooling. Nothing to manage, nothing to drift.</p>
<hr>
<h2 id="secrets-without-headaches-sops--age">Secrets Without Headaches: SOPS + Age</h2>
<p>The natural objection to &ldquo;everything is in git&rdquo;: what about passwords?</p>
<p>The answer is <a href="https://github.com/mozilla/sops">SOPS</a> with age keys. Secrets are encrypted in the repository - committed plaintext is never stored - and decrypted at activation time by the <code>sops-nix</code> NixOS module. Each secret is mounted as a file with explicit ownership and permissions.</p>
<p>The PowerDNS module shows this concretely. The primary DNS server needs an API key, and that key must be accessible to the service at runtime but must never appear in plaintext in git or the Nix store:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>config <span style="color:#f92672">=</span> mkIf cfg<span style="color:#f92672">.</span>enable {
</span></span><span style="display:flex;"><span>  sops<span style="color:#f92672">.</span>templates<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;pdns-env&#34;</span> <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkIf (<span style="color:#f92672">!</span>cfg<span style="color:#f92672">.</span>secondary) {
</span></span><span style="display:flex;"><span>    content <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      PDNS_API_KEY=</span><span style="color:#e6db74">${</span>config<span style="color:#f92672">.</span>sops<span style="color:#f92672">.</span>placeholder<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;powerdns/api-key&#34;</span><span style="color:#e6db74">}</span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    owner <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pdns&#34;</span>;
</span></span><span style="display:flex;"><span>    group <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;pdns&#34;</span>;
</span></span><span style="display:flex;"><span>    mode  <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;0400&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  services<span style="color:#f92672">.</span>powerdns <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    extraConfig <span style="color:#f92672">=</span> baseExtraConfig;
</span></span><span style="display:flex;"><span>    secretFile <span style="color:#f92672">=</span> lib<span style="color:#f92672">.</span>mkIf (<span style="color:#f92672">!</span>cfg<span style="color:#f92672">.</span>secondary) config<span style="color:#f92672">.</span>sops<span style="color:#f92672">.</span>templates<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;pdns-env&#34;</span><span style="color:#f92672">.</span>path;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p><code>config.sops.placeholder.&quot;powerdns/api-key&quot;</code> is a reference to an encrypted value in <code>secrets/</code>. During activation, <code>sops-nix</code> decrypts it and writes the rendered template to a path in <code>/run/secrets/</code>, owned by <code>pdns:pdns</code>, mode 0400. <code>services.powerdns.secretFile</code> tells the service to load environment variables from that path. The API key never appears in the Nix store. The secret file in <code>secrets/</code> is committed encrypted with age - anyone with the host&rsquo;s age key (generated at first boot, stored in <code>/etc/ssh/ssh_host_ed25519_key</code>) can decrypt it; everyone else sees ciphertext.</p>
<p>This pattern applies uniformly to database passwords, API keys, WireGuard private keys, and anything else that shouldn&rsquo;t be readable by arbitrary processes. The <code>secrets/</code> directory lives in the same repo as everything else. The repo truly is the single source of truth.</p>
<hr>
<h2 id="deploying-a-new-machine-in-one-command">Deploying a New Machine in One Command</h2>
<p>New machine provisioning follows a single script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nix run github:nix-community/nixos-anywhere -- <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --flake .#<span style="color:#e6db74">&#34;</span>$NAME<span style="color:#e6db74">&#34;</span> <span style="color:#e6db74">&#34;</span>$REMOTE<span style="color:#e6db74">&#34;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  --generate-hardware-config nixos-generate-config <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;./hosts/</span><span style="color:#e6db74">${</span>NAME<span style="color:#e6db74">}</span><span style="color:#e6db74">/hardware-configuration.nix&#34;</span>
</span></span></code></pre></div><p><code>nixos-anywhere</code> SSHs into a running NixOS installer (booted from a USB stick or network image), generates hardware configuration for the specific machine, partitions disks according to the host&rsquo;s <code>disko.nix</code>, and installs the full system from the flake. The only prerequisites are a host config in <code>hosts/$NAME/</code> and SSH access to the target.</p>
<p>Disk layout is declared per-host in <code>disko.nix</code>. LUKS encryption, partition sizes, filesystem types, subvolumes - all expressed as Nix. The <code>itw</code> host has an encrypted system partition, a separate encrypted <code>/games</code> partition with a keyfile, and an NTFS Windows partition, all described in code and applied automatically during provisioning.</p>
<p>After the initial bootstrap, all future updates are <code>nho</code> - atomic, with rollback available from the bootloader. The setup cost is one remote command; the ongoing cost is nearly zero.</p>
<p>The PowerDNS setup illustrates what this means at scale. Two servers - <code>schere</code> (primary) and <code>stein</code> (secondary) - run the DNS infrastructure. Both configs live in the same repo. Adding or changing a DNS-related NixOS option is one file edit, then two deploys: <code>nho --hostname schere</code> and <code>nho --hostname stein</code>. No config management layer to synchronize, no state to reconcile, no &ldquo;did I update both servers?&rdquo; uncertainty.</p>
<hr>
<h2 id="the-hard-parts">The Hard Parts</h2>
<p>In the interest of accuracy: NixOS has rough edges.</p>
<p><strong>The language.</strong> Nix is lazy and functional, with an evaluation model that can surprise you. Error messages have improved significantly in recent Nix versions, but a type mismatch deep in a module evaluation can still produce a backtrace that takes time to interpret. The learning curve is front-loaded; it gets easier, but the first weeks are real.</p>
<p><strong><code>nixos-unstable</code> breakage.</strong> This config tracks <code>nixos-unstable</code>, which means <code>nix flake update</code> occasionally introduces a broken package or module API change. The fix is usually in nixpkgs within a day or two, and rollback handles the immediate problem - but it does happen. The stable channels are calmer.</p>
<p><strong>Proprietary FHS binaries.</strong> Most software works fine. Occasionally a binary with hardcoded library paths needs a <code>buildFHSEnv</code> wrapper. It&rsquo;s ten lines of Nix and not scary once you&rsquo;ve done it once, but it&rsquo;s a known rough edge.</p>
<p><strong>First-time secure boot setup.</strong> Getting lanzaboote (Secure Boot), LUKS encryption, and disko all working correctly on first install has a real setup cost. The pieces fit together well once configured, but the first time through the documentation takes effort.</p>
<p><strong>Honest scope assessment.</strong> If you have one machine and enjoy imperative tinkering, the learning investment may not be worth the return. NixOS pays off at scale, for reproducibility across machines, or for users who want the peace of mind that comes from knowing exactly what&rsquo;s on each system. Single-machine casual users are not the target audience.</p>
<hr>
<h2 id="where-this-leads">Where This Leads</h2>
<p>A year ago, provisioning a new server meant: boot the installer, partition manually, install the base system, copy configs from another machine, run Ansible, debug what drifted, repeat. With this setup, it&rsquo;s <code>./install-remote.sh newhost root@192.168.1.x</code> and wait. The system that comes up is defined entirely by the config in git, with no variation possible.</p>
<p>That mental shift - from &ldquo;the system is the result of what I&rsquo;ve done to it&rdquo; to &ldquo;the system is a function of what I&rsquo;ve declared&rdquo; - is the core of what NixOS offers.</p>
<p>A few threads worth pulling on from here:</p>
<p><strong>disko and LUKS in depth.</strong> The declarative disk setup deserves its own writeup. Combining disko, LUKS, and lanzaboote (Secure Boot) on a single machine, from a declarative base, is a solved problem that&rsquo;s worth documenting step by step.</p>
<p><strong>Writing your own NixOS module.</strong> The <code>ox.*</code> namespace in this repo is a collection of custom modules. The pattern - options, defaults, conditional config - is learnable in an afternoon and immediately useful for your own abstraction layer.</p>
<p><strong>home-manager on non-NixOS.</strong> The home-manager configuration described here works on any Linux (or macOS) system, not just NixOS. If NixOS on bare metal is too much commitment, running home-manager standalone on Debian or Ubuntu gets you the dotfile management and reproducible user environment without changing the OS.</p>
<p>Where to start: <a href="https://nixos.org">nixos.org</a> → <a href="https://nix.dev">nix.dev</a> → find a real flake on GitHub and read it → start with one machine.</p>
<p>The whole system is a function. Write better inputs; get a better system. That&rsquo;s the deal.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
