<?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>Nix on paradigmatic.systems</title>
    <link>https://paradigmatic.systems/tags/nix/</link>
    <description>Recent content in Nix on paradigmatic.systems</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sat, 03 Oct 2026 19:00:00 -0400</lastBuildDate><atom:link href="https://paradigmatic.systems/tags/nix/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Rhizomatic Systems; Or, the Maximum Rizz YOLO Homelab</title>
      <link>https://paradigmatic.systems/posts/rhizomatic-systems/</link>
      <pubDate>Sat, 03 Oct 2026 19:00:00 -0400</pubDate>
      
      <guid>https://paradigmatic.systems/posts/rhizomatic-systems/</guid>
      <description>A NixOS monorepo pattern where everything can see everything, and updates arrive as CI commits.</description>
      <content:encoded><![CDATA[<h2 id="the-tree-and-the-rhizome">The Tree and the Rhizome</h2>
<p>Systems models are typically <em>arborescent</em>, or tree-like. There is a root source of truth, and everything else extends from there. Kubernetes has its API server. Ansible has whichever laptop you ran the playbook from. Terraform has a state file with a lock. Even my own <a href="/posts/setting-up-deploy-rs">deploy-rs setup</a> had a root: one machine with the keys, pushing closures outward to nodes that could only receive. That&rsquo;s a fine shape when an ops team <em>is</em> the root. For one person with a few computers, it means one of them is special, and the whole system is only as available as that one.</p>
<figure class="align-center ">
    <img loading="lazy" src="/rhizome-vs-tree.jpg#center"
         alt="A tree with a single trunk and branching roots beside a rhizome, a tangle of shoots and roots with no center"/> <figcaption>
            <p>Tree and rhizome.
                    <a href="https://commons.wikimedia.org/wiki/File:Rhizome_vs_tree.jpg">Drawing by Magda Wojtyra and Marc Ngui, CC BY-SA 4.0, via Wikimedia Commons</a></p>
        </figcaption>
</figure>

<p>A <em>rhizome</em>, like ginger or crabgrass, is a network of connected points. In <a href="https://en.wikipedia.org/wiki/A_Thousand_Plateaus"><em>A Thousand Plateaus</em></a>, Deleuze and Guattari lay out some of the principles of <em>rhizomatic thought</em>. A rhizome connects different types of things together, with no central pivot. A rhizome may be broken at any point, and it starts up again along its old lines or along new ones. Finally, they insist on the <em>map</em> over the <em>tracing</em>: a tracing copies a structure that was decided in advance, while a map is drawn from whatever actually connected and stays open to redrawing.</p>
<p>But this isn&rsquo;t a philosophy post. It&rsquo;s about how the endgame of my fleet configuration ended up taking a rhizomatic shape due to a handful of decisions.</p>
<h2 id="the-fleet-config-monorepo">The Fleet Config Monorepo</h2>
<p>If you install NixOS on a single computer, you end up with a <code>/etc/nixos/configuration.nix</code> which is the source code for the computer, usually importing an auto-generated <code>hardware-configuration.nix</code>. If you install NixOS on several computers, you end up with several of those. They&rsquo;ll tend to have some overlap, on everything from timezones to your preferred dev tooling. If you want to upgrade something, you&rsquo;ll repeat the same process on each computer. If you want to add cross-cutting functionality, well, you get the idea.</p>
<p>The move then is to create a single <code>flake.nix</code> whose schema provides a top-level <code>nixosConfigurations</code>. Your fleet becomes an attrset. Forgiving some pseudo-Nix for the rest of the post:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  desktop <span style="color:#f92672">=</span> { <span style="color:#f92672">...</span> };
</span></span><span style="display:flex;"><span>  laptop <span style="color:#f92672">=</span> { <span style="color:#f92672">...</span> };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This of course allows you to abstract out shared segments of their definitions:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span><span style="color:#66d9ef">let</span> devTools <span style="color:#f92672">=</span> [ firefox git ]; <span style="color:#66d9ef">in</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  desktop <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    packages <span style="color:#f92672">=</span> devTools <span style="color:#f92672">++</span> [ <span style="color:#f92672">...</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  laptop <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    packages <span style="color:#f92672">=</span> devTools <span style="color:#f92672">++</span> [ <span style="color:#f92672">...</span> ];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>It&rsquo;s a pretty neat way to parameterize any number of machines under a single source of truth!</p>
<p>One of the first steps in <em>Rhizome</em>-hood is to introduce a <em>forge</em>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  vps <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>forgejo<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></code></pre></div><p>Its role is to</p>
<ol>
<li>Host the config repo (among others)</li>
<li>Run CI; in particular, a <code>check.yml</code> that builds all the hosts on every PR. Breaking changes don&rsquo;t make it in.</li>
</ol>
<p>I want the forge highly available, so I host it on my low-resource VPS, and my home laptop, which has the RAM for it, serves as the CI runner.</p>
<p>Notice what that does to the shape. The config is no longer administered from outside the fleet by one special machine with the keys. It is hosted, built, and checked by members of the fleet it describes.</p>
<h2 id="any-node-reaches-any-node">Any Node Reaches Any Node</h2>
<p><a href="https://github.com/slackhq/nebula">Nebula</a> is a nifty tool from Slack that lets you define a network overlay with certificate-based identities.
One well-known node (the &ldquo;lighthouse&rdquo;) acts as a rendezvous point, not a relay. Nodes usually talk directly once they find each other, even from behind NAT.</p>
<p>The VPS is the obvious choice for the lighthouse since it has a stable public IP. Making use of the <code>rec</code> keyword for a recursively evaluated attr-set, we can co-configure the nodes like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span><span style="color:#66d9ef">rec</span> {
</span></span><span style="display:flex;"><span>  vps <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>nebula<span style="color:#f92672">.</span>networks<span style="color:#f92672">.</span>rhizome <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>      isLighthouse <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>  laptop <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>nebula<span style="color:#f92672">.</span>networks<span style="color:#f92672">.</span>rhizome <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>      lighthouses <span style="color:#f92672">=</span> [ vps ];  <span style="color:#75715e"># really the lighthouse&#39;s overlay IP</span>
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now every node has a stable address on a flat private subnet, no matter where it physically is. The payoff is that the low-resource VPS never has to build anything. It delegates every Nix build to the laptop over the overlay:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  vps <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    nix<span style="color:#f92672">.</span>settings<span style="color:#f92672">.</span>max-jobs <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;  <span style="color:#75715e"># never build locally</span>
</span></span><span style="display:flex;"><span>    nix<span style="color:#f92672">.</span>buildMachines <span style="color:#f92672">=</span> [{ hostName <span style="color:#f92672">=</span> laptop; sshUser <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;nixremote&#34;</span>; }];
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The same overlay is how I SSH into the laptop from anywhere, with its <code>sshd</code> closed to everything but the Nebula interface. Other tricks follow, like an nginx reverse proxy on the VPS serving websites that actually run at home.</p>
<h2 id="automate-the-updates">Automate the Updates</h2>
<p>Here&rsquo;s where we start to close the loop, via 3 different update modes.</p>
<ol>
<li>Push: Updates to the VPS trigger a CI job wrapping the <code>deploy-rs</code> step.</li>
<li>Pull: Laptops get a systemd timer that checks the forge for main&rsquo;s tip, then builds and switches to that commit.</li>
<li>Self-Bump: Apps running in the fleet are flake inputs. A green merge triggers a version bump in the config repo.</li>
</ol>
<p>All the friction in <a href="/posts/decoupling-deployment-model">Decoupling My Deployment Model</a> just becomes automated CI steps.</p>
<h3 id="push">Push</h3>
<p>The push to the VPS is the <code>deploy-rs</code> command from the earlier posts, run by CI instead of me. It doesn&rsquo;t need to happen on every merge, though, so the workflow only fires when the push asks for it. A commit trailer turns out to be the cheapest &ldquo;deploy this&rdquo; flag there is:</p>
<pre tabindex="0"><code>git commit -m &#34;Tweak nginx&#34; -m &#34;Deploy: vps&#34;
</code></pre><p>The workflow reads it with <code>git interpret-trailers</code> and runs <code>nix run .#deploy-rs -- .#vps</code>. No second system, no deploy button, just a line in the commit message.</p>
<h3 id="pull">Pull</h3>
<p>Laptops don&rsquo;t accept inbound connections, so instead of being pushed to they poll. A systemd timer asks the forge for the tip of <code>main</code>, and if it isn&rsquo;t what&rsquo;s running, switches to exactly that commit:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  laptop <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    systemd<span style="color:#f92672">.</span>timers<span style="color:#f92672">.</span>fleet-switch<span style="color:#f92672">.</span>timerConfig<span style="color:#f92672">.</span>OnCalendar <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;*:0/5&#34;</span>;
</span></span><span style="display:flex;"><span>    systemd<span style="color:#f92672">.</span>services<span style="color:#f92672">.</span>fleet-switch<span style="color:#f92672">.</span>script <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      rev=$(git ls-remote $FORGE/fleet.git main | cut -f1)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      [ &#34;$rev&#34; = &#34;$(nixos-version --configuration-revision)&#34; ] &amp;&amp; exit 0
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      nixos-rebuild switch --flake &#34;git+$FORGE/fleet.git?rev=$rev#laptop&#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>}
</span></span></code></pre></div><p>The comparison works because the flake stamps every build with the commit it came from (<code>system.configurationRevision = self.rev</code>). Pinning the exact revision it observed means a laptop that was asleep for a week catches up to the same commit as everyone else, not to whatever <code>main</code> happens to be mid-build.</p>
<h3 id="self-bump">Self-Bump</h3>
<p>Apps that run on the fleet are flake inputs. Each one&rsquo;s own CI, after a green merge, runs a single step:</p>
<pre tabindex="0"><code>nix run $FORGE/fleet.git#bump -- myapp=$sha
</code></pre><p>That re-pins the input, runs <code>nix flake check</code> with the new lock (a bump the fleet can&rsquo;t build never lands), and pushes to <code>main</code>. A small map in the fleet repo says which hosts a moved pin should deploy:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>rolling <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  myapp <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;laptop&#34;</span> ];
</span></span><span style="display:flex;"><span>  website <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;vps&#34;</span> ];
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>That one line and the step above are all the app and the fleet know about each other. The &ldquo;tab back to the server repo, <code>nix flake update</code>, <code>deploy</code>&rdquo; loop is gone.</p>
<h3 id="tool-bumps">Tool Bumps</h3>
<p>The same trick works pointed at upstream instead of my own repos. Claude Code and Codex are pinned independently of <code>nixpkgs</code>, because the model gate is server-side: a CLI older than a new model locks every agent on the fleet out of it the day it ships. So a daily workflow compares the pins with what Anthropic and npm publish, rewrites them, checks, and pushes with a <code>Deploy: vps</code> trailer:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">on</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">schedule</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">cron</span>: <span style="color:#e6db74">&#34;17 6 * * *&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">jobs</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">bump</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">runs-on</span>: <span style="color:#ae81ff">native</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">steps</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#f92672">run</span>: <span style="color:#ae81ff">nix run .#bump-tools</span>
</span></span></code></pre></div><p>These show up in <code>main</code>&rsquo;s history:</p>
<pre tabindex="0"><code>6f84965 bump tools: claude-code 2.1.287 -&gt; 2.1.288
5ddc48f bump tools: claude-code 2.1.286 -&gt; 2.1.287, codex 0.159.3 -&gt; 0.160.0
</code></pre><h2 id="yolo-mode-fleet-wide-features">YOLO Mode: Fleet-Wide Features</h2>
<p>With these pieces in place, one PR in one repo can orchestrate cross-cutting features. Every host lives in the same repo, CI builds all of them on every PR, and the merge deploys itself. That makes it a great place to let an agent loose, since a fleet-wide change is a single reviewable diff with a build gate in front of it.</p>
<p>As an example, I asked for an observability system using <a href="https://ntfy.sh">ntfy</a>. The topic name is the whole credential, so it lives in a secret, and the app on my phone subscribes to it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  vps<span style="color:#f92672">.</span>fleet<span style="color:#f92672">.</span>notify<span style="color:#f92672">.</span>watch<span style="color:#f92672">.</span>laptop <span style="color:#f92672">=</span> { host <span style="color:#f92672">=</span> laptop; port <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>; };
</span></span><span style="display:flex;"><span>  laptop<span style="color:#f92672">.</span>fleet<span style="color:#f92672">.</span>notify<span style="color:#f92672">.</span>watch<span style="color:#f92672">.</span>vps <span style="color:#f92672">=</span> { host <span style="color:#f92672">=</span> vps; port <span style="color:#f92672">=</span> <span style="color:#ae81ff">22</span>; };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The hosts probe each other, and report each transition, from reachable to silent and back again. Any service unit can also wire in:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>systemd<span style="color:#f92672">.</span>services<span style="color:#f92672">.</span>backup<span style="color:#f92672">.</span>onFailure <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;notify-failed@%n.service&#34;</span> ];
</span></span></code></pre></div><p>This way, if there&rsquo;s a rupture in the flows, I&rsquo;ll know about it quickly.</p>
<h2 id="the-knots">The Knots</h2>
<p>A rhizome can still tie itself in a loop, and that&rsquo;s where some actual problem-solving went.</p>
<p>A cross-cutting change triggers a re-deploy of the VPS and the laptop. That means the CI runner deploys itself. A switch that restarts the runner kills every job inside it, including the <code>deploy-rs</code> activation of the VPS. Two rules untangle it. The laptop&rsquo;s pull updater drains the runner before it switches, so no new jobs start mid-deploy. And the updater and CI&rsquo;s VPS deploy share a single <code>flock</code>, so replacing the runner has to wait for any in-flight deploy to finish, and vice versa.</p>
<p>Secrets are the other knot, since they&rsquo;re the one thing that can&rsquo;t be declarative. <a href="https://github.com/Mic92/sops-nix">sops-nix</a> gets most of the way: each file is encrypted for my admin key plus the hosts that consume it, so &ldquo;move a service to another host&rdquo; is add a recipient, re-key, commit.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">creation_rules</span>:
</span></span><span style="display:flex;"><span>  - <span style="color:#f92672">path_regex</span>: <span style="color:#ae81ff">secrets/myapp\.yaml$</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">key_groups</span>: [{ <span style="color:#f92672">age</span>: [ <span style="color:#75715e">*admin,</span> <span style="color:#75715e">*laptop</span> ] }]
</span></span></code></pre></div><p>But the Nebula certificates are still signed by hand from a CA directory and copied out of band. Maybe the rhizome has a root after all. Oops!</p>
<h2 id="summary">Summary</h2>
<p>In my <em>rhizomatic</em> system,</p>
<ul>
<li>the config is a member of the fleet it describes</li>
<li>every node can reach every other</li>
<li><code>main</code> is converged on from three directions</li>
<li>every edge reports its own failures</li>
</ul>
<p>Heterogeneous things connected with no pivot, rupturable anywhere, and a map rather than a tracing. Together they mean I&rsquo;m basically out of the loop, and the commit log on <code>main</code> reads like the fleet maintaining itself.</p>
<p><em>As usual, <a href="https://happysleepy.com/wp-content/uploads/A-Thousand-Plateaus-Drawings-10000BC-49b.jpg">Deleuze and Guattari (illustrated by Marc Ngui and Magda Wojtyra)</a> should help clarify matters.</em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Decoupling My Deployment Model</title>
      <link>https://paradigmatic.systems/posts/decoupling-deployment-model/</link>
      <pubDate>Sat, 11 Jul 2026 07:22:30 -0400</pubDate>
      
      <guid>https://paradigmatic.systems/posts/decoupling-deployment-model/</guid>
      <description>Evolving a NixOS deploy-rs setup where each site is a flake input, the whole server builds atomically, and multi-node deploys run with one command.</description>
      <content:encoded><![CDATA[<p>In my previous <a href="/posts/setting-up-deploy-rs">introduction to deploy-rs</a> I shared a particular model of NixOS server deployment that I found very interesting:</p>
<ol>
<li>Different components (websites) are treated as flake inputs.</li>
<li>The <code>nginx</code> config refers to the packaged outputs.</li>
<li>The entire server only builds if all the components build.</li>
<li>Deployments are atomic and minimal; the exact system is copied up.</li>
</ol>
<p>This idea becomes particularly powerful once you have multiple nodes and can write <a href="/posts/integration-containers">integration tests</a> for your entire system then deploy across multiple machines with a single command.</p>
<p>But for my personal use case I started to notice some slight friction. Say I&rsquo;m updating this website with a new post. I&rsquo;m working in its repo. Once I&rsquo;m done, I tab back to the server config repo, run a <code>nix flake update</code> to pull in and re-pin the website, then <code>deploy</code>. It makes logical sense but doesn&rsquo;t feel as ergonomic as it could.</p>
<p>The system also breaks down with more <em>granular</em> deployments. Say I want to give an <em>untrusted tenant</em> the ability to update <code>paradigmatic.systems</code> without interfering with the other services running on the same server. It&rsquo;s not an unusual use case, and <code>deploy-rs</code> is designed with it in mind. Now I&rsquo;ve finally taken the time to migrate from <em>node-based</em> to <em>profile-based</em> deployment.</p>
<h2 id="attempting-to-keep-using-deploy-rs">Attempting to Keep Using deploy-rs</h2>
<p>The <code>deploy-rs</code> API is built for this, and it should be a simple change!</p>
<h3 id="in-the-server-repo">In the Server Repo</h3>
<p>I removed <code>paradigmatic-systems</code> from the flake inputs and instead defined an unprivileged deploy user:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>users<span style="color:#f92672">.</span>users<span style="color:#f92672">.</span>paradigmatic-deploy <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  isSystemUser <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  group <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;paradigmatic-deploy&#34;</span>;
</span></span><span style="display:flex;"><span>  createHome <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>  home <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/var/lib/paradigmatic-deploy&#34;</span>;
</span></span><span style="display:flex;"><span>  shell <span style="color:#f92672">=</span> pkgs<span style="color:#f92672">.</span>bashInteractive;
</span></span><span style="display:flex;"><span>  openssh<span style="color:#f92672">.</span>authorizedKeys<span style="color:#f92672">.</span>keys <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>    <span style="color:#e6db74">&#34;ssh-ed25519 AAAA...&#34;</span>
</span></span><span style="display:flex;"><span>  ];
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>users<span style="color:#f92672">.</span>groups<span style="color:#f92672">.</span>paradigmatic-deploy <span style="color:#f92672">=</span> { };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>nix<span style="color:#f92672">.</span>settings<span style="color:#f92672">.</span>trusted-users <span style="color:#f92672">=</span> [ <span style="color:#e6db74">&#34;paradigmatic-deploy&#34;</span> ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ensure the profile directory exists before the first deploy</span>
</span></span><span style="display:flex;"><span>systemd<span style="color:#f92672">.</span>tmpfiles<span style="color:#f92672">.</span>rules <span style="color:#f92672">=</span> [
</span></span><span style="display:flex;"><span>  <span style="color:#e6db74">&#34;d /nix/var/nix/profiles/per-user/paradigmatic-deploy 0755 paradigmatic-deploy paradigmatic-deploy -&#34;</span>
</span></span><span style="display:flex;"><span>];
</span></span></code></pre></div><p>Yes, I&rsquo;ve contradicted myself by making <code>paradigmatic-deploy</code> a trusted user. I&rsquo;ll come back to that later. But for now let&rsquo;s say &ldquo;we don&rsquo;t think they&rsquo;re going to upload malware, they just need guardrails from the other services&rdquo;.</p>
<p>Then I proxy the website to the actual user profile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>services<span style="color:#f92672">.</span>nginx <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>  virtualHosts<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;paradigmatic.systems&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    root <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;/nix/var/nix/profiles/per-user/paradigmatic-deploy/website&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>In NixOS, a profile is a stable and mutable pointer into the (immutable) Nix store. It&rsquo;s a symlink managed as a series of numbered generations. For a desktop environment it might wrap your entire system closure, including your kernel, services, and every installed package. In this case it&rsquo;s just a single website!</p>
<h3 id="in-the-website-repo">In the Website Repo</h3>
<p>We add <code>deploy-rs</code> to the website flake inputs, and configure it to set the <code>website</code> profile:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>deploy<span style="color:#f92672">.</span>nodes<span style="color:#f92672">.</span>paradigmatic-systems <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  hostname <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;my-node&#34;</span>;
</span></span><span style="display:flex;"><span>  profiles<span style="color:#f92672">.</span>website <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>    sshUser <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;paradigmatic-deploy&#34;</span>;
</span></span><span style="display:flex;"><span>    user <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;paradigmatic-deploy&#34;</span>;
</span></span><span style="display:flex;"><span>    path <span style="color:#f92672">=</span> deploy-rs<span style="color:#f92672">.</span>lib<span style="color:#f92672">.</span>x86_64-linux<span style="color:#f92672">.</span>activate<span style="color:#f92672">.</span>profile
</span></span><span style="display:flex;"><span>      self<span style="color:#f92672">.</span>packages<span style="color:#f92672">.</span>x86_64-linux<span style="color:#f92672">.</span>website;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p><code>profiles.website</code> is the rendezvous point for the two flakes. The <code>path</code> attribute is what actually gets copied up. The <code>activate.profile</code> wrapper is an activation hook that <code>deploy-rs</code> uses to verify the deployment. It&rsquo;s also the villain of this story&hellip;</p>
<h2 id="the-snag">The Snag</h2>
<p>The above code worked beautifully from my NixOS machine. I was excited to demonstrate the system to my &ldquo;untrusted tenant&rdquo; and went to run the deployment from her macOS machine. My eyes darted across <code>deploy-rs.lib.x86_64-linux.activate.profile</code> and briefly questioned things before seeing a blaring <code>System Mismatch</code> error.</p>
<p>The activation wrapper and the host system require different architectures. To build it correctly I&rsquo;d need to pull in an entire Linux toolchain. <a href="https://nix.dev/tutorials/cross-compilation.html">Cross-compilation</a> is pretty robust, but how much do I want to beef up my flake for a static website that won&rsquo;t differ from one architecture to the next!?</p>
<h2 id="an-easy-escape-hatch">An Easy Escape Hatch</h2>
<p>One quick option offered by <code>deploy-rs</code> is the <code>remoteBuild = true</code> flag which pushes the derivation up and has the server build it on that end. Yes, it&rsquo;s that simple. However, my cheap-o server is sized for the services it runs; not for pulling in the whole build-time closure.</p>
<h2 id="switching-it-out-to-nix-copy">Switching it Out to nix-copy</h2>
<p>In the interest of moving forward with my life I opted to go under the hood to the same core Nix tools used by <code>deploy-rs</code>.
The server flake remained basically the same! In the website flake I defined a simple deploy app:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>  deploy-website <span style="color:#f92672">=</span> pkgs<span style="color:#f92672">.</span>writeShellApplication {
</span></span><span style="display:flex;"><span>    name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;deploy-website&#34;</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">      target=&#34;</span><span style="color:#ae81ff">&#39;&#39;$</span><span style="color:#e6db74">{DEPLOY_TARGET:-paradigmatic-deploy@my-node}&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      site=&#34;</span><span style="color:#e6db74">${</span>self<span style="color:#f92672">.</span>packages<span style="color:#f92672">.</span><span style="color:#e6db74">${</span>system<span style="color:#e6db74">}</span><span style="color:#f92672">.</span>website<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      nix copy --no-check-sigs --to &#34;ssh://$target&#34; &#34;$site&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      echo &#34;==&gt; Activating&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      ssh &#34;$target&#34; nix-env --profile /nix/var/nix/profiles/per-user/paradigmatic-deploy/website --set &#34;$site&#34;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      echo &#34;==&gt; Deployed https://paradigmatic.systems&#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></code></pre></div><p>All we&rsquo;re doing is copying up the package and making the profile switch. The <code>--no-check-sigs</code> flag is the counterpart of being in the <code>trusted-users</code> list. Let&rsquo;s talk about that briefly.</p>
<h2 id="the-threat-model">The Threat Model</h2>
<p>I&rsquo;ve given a fairly powerful capability to what I called an &ldquo;untrusted&rdquo; user, so I should probably address that a bit. Realistically, anyone who can write to the shared <code>/nix/store</code> is within my trust boundary.</p>
<p>Signing keys improve the situation with respect to accidents and shallow compromises. The trust is revocable and auditable, and composes into real CI architecture. But the tenant can still inject arbitrary store paths.</p>
<p>For handling a true scumbag tenant the design changes shape completely. They never get store write access whatsoever. They push up source and the server builds on its own terms. That, or they get a dedicated container or VM with its own store.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I needed more granularity in my deployment, so I broke out a piece of my server config. Instead of being baked in, it&rsquo;s configured with a symlink that&rsquo;s switchable by a specific user.</p>
<p>To route around a mild inconvenience I dropped <code>deploy-rs</code> in favor of Nix primitives. I&rsquo;m using the same underlying manipulation of the nix store, so I still get atomicity, history, and garbage collection.</p>
<p>The main thing I lost is the &ldquo;magic rollback&rdquo; feature of deploy-rs where the system auto-reverts if SSH becomes unreachable. However, for this class of deployment (a static site swap) it&rsquo;s not a big risk.</p>
<p><code>deploy-rs</code> is a powerful tool for multi-node or multi-profile orchestration with interactive confirmation, and it&rsquo;s on my roadmap to work on some more interesting examples later!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>NixOS Integration Tests To Become Even More Undeniable</title>
      <link>https://paradigmatic.systems/posts/integration-containers/</link>
      <pubDate>Sun, 15 Mar 2026 04:30:00 +0000</pubDate>
      
      <guid>https://paradigmatic.systems/posts/integration-containers/</guid>
      <description>NixOS&amp;#39; QEMU-based integration tests spin up whole multi-node systems in CI—why that makes them even more undeniable than container-based testing.</description>
      <content:encoded><![CDATA[<p>I&rsquo;ve mentioned the <a href="/posts/first-principles-infrastructure">insane power</a> of NixOS&rsquo; QEMU-based integration tests.</p>
<details style="border: 2px solid #374151; border-radius: 8px; padding: 16px; background-color: #1f2937; margin: 16px 0;">
<summary style="cursor: pointer; font-weight: 600; color: #60a5fa;">If you're wondering why you (yes, you!) should care at all about this random Linux distro, expand this carat for my summary.</summary>
<div style="margin-top: 12px;">
For those not in the know, a **NixOS configuration** is a data structure that fully determines a Linux system, from the kernel to the users and the systemd processes. It's a highly reproducible artifact backed by the largest open-source package repo in existence. You can:
<ul>
<li>Put it in version control and use it for your daily driver to make hardware migrations painless</li>
<li>Deploy to bare metal VPS with low-resource <a href="/posts/setting-up-deploy-rs">&ldquo;build-then-push&rdquo; tooling</a></li>
<li>Build Docker/OCI containers from it to appease corporate goons</li>
<li>Run it in CI/CD pipelines</li>
<li>Share it with teammates for identical dev environments</li>
<li>Parameterize it to define a cluster of nodes</li>
</ul>
</div>
</details>
<br/>
<p>The integration test framework lets you define virtualized nodes with pass/fail conditions on the behavior of the components therein. Furthermore the nodes can be embedded within an arbitrary network topology to test the overall system-of-systems behavior. With the tests defined, they&rsquo;re wrapped into the very notion of validity for your configuration(s).</p>
<p>This system is obviously the future of infrastructure. NixOS is doing things that are deep on the long-term roadmap for other system-of-system solutions. You could burn serious man-hours approximating this capability in <code>docker-compose</code> and end up nowhere near the raw reproducible bare-metal power.</p>
<p>However, in my own experiments, the virtualization layer quickly made my $300 dev laptop smoke at the ears for anything beyond 1 node. That&rsquo;s why I&rsquo;m excited that <a href="https://nixcademy.com/posts/faster-cheaper-nixos-integration-tests-with-containers/">Nixcademy has announced</a> a new feature. An unnamed corporate patron has supported development of namespace-based containerization backed by <code>systemd-nspawn</code>.</p>
<p>Here&rsquo;s an abbreviated example illustrating how I could bake this very post into my VPS deployment!</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  description <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;NixOS configuration with test&#34;</span>;
</span></span><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/nixos-unstable&#34;</span>;
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>  outputs <span style="color:#f92672">=</span> { self<span style="color:#f92672">,</span> nixpkgs }: 
</span></span><span style="display:flex;"><span>  {
</span></span><span style="display:flex;"><span>    checks<span style="color:#f92672">.</span>x86_64-linux<span style="color:#f92672">.</span>test <span style="color:#f92672">=</span> <span style="color:#66d9ef">let</span>
</span></span><span style="display:flex;"><span>      pkgs <span style="color:#f92672">=</span> nixpkgs<span style="color:#f92672">.</span>legacyPackages<span style="color:#f92672">.</span>x86_64-linux;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">in</span> pkgs<span style="color:#f92672">.</span>testers<span style="color:#f92672">.</span>runNixOSTest {
</span></span><span style="display:flex;"><span>      
</span></span><span style="display:flex;"><span>      name <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;configuration-test&#34;</span>;
</span></span><span style="display:flex;"><span>      
</span></span><span style="display:flex;"><span>      nodes<span style="color:#f92672">.</span>server <span style="color:#f92672">=</span> { config<span style="color:#f92672">,</span> pkgs<span style="color:#f92672">,</span> paradigmatic<span style="color:#f92672">,</span> <span style="color:#f92672">...</span> }: {
</span></span><span style="display:flex;"><span>        services<span style="color:#f92672">.</span>nginx <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>          virtualHosts<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;_&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>            root <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>paradigmatic<span style="color:#f92672">.</span>packages<span style="color:#f92672">.</span><span style="color:#e6db74">${</span>system<span style="color:#e6db74">}</span><span style="color:#f92672">.</span>website<span style="color:#e6db74">}</span><span style="color:#e6db74">&#34;</span>;
</span></span><span style="display:flex;"><span>          }
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>      };
</span></span><span style="display:flex;"><span>      
</span></span><span style="display:flex;"><span>      nodes<span style="color:#f92672">.</span>client <span style="color:#f92672">=</span> { config<span style="color:#f92672">,</span> pkgs<span style="color:#f92672">,</span> <span style="color:#f92672">...</span> } : {};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>      testScript <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        server.start()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        client.start()
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        server.wait_for_unit(&#34;nginx.service&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        client.wait_for_unit(&#34;multi-user.target&#34;)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        client.succeed(&#34;curl http://server/posts/integration-containers 
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        | grep &#39;Hello from this very text block!&#39;&#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>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Nice!</p>
<p><em>Want to chat about NixOS adoption? <a href="mailto:rorih@live.com">Email me</a></em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Nix: the Language, the Tool, the Package Repo, the OS</title>
      <link>https://paradigmatic.systems/posts/nix-nixpkgs-nixos/</link>
      <pubDate>Tue, 10 Jun 2025 04:30:00 +0000</pubDate>
      
      <guid>https://paradigmatic.systems/posts/nix-nixpkgs-nixos/</guid>
      <description>An approachable explainer untangling the four things all called &amp;#39;Nix&amp;#39;—the language, the tool, the nixpkgs repo, and the NixOS operating system.</description>
      <content:encoded><![CDATA[<p>There&rsquo;s a lot going on in the Nix ecosystem. If you haven&rsquo;t slogged through <a href="https://edolstra.github.io/pubs/phd-thesis.pdf">the thesis</a> then it might seem downright opaque. This is my attempt at an easy explainer for newcomers.</p>
<h2 id="the-tool-and-the-language">The Tool and the Language</h2>
<p>Let&rsquo;s bootstrap ourselves with some circular facts.</p>
<blockquote>
<p>Nix is a <em>tool</em> for building (or &ldquo;packaging&rdquo;) <em>software components</em> using instructions expressed in the Nix <em>language</em>.
The Nix language is even able to <a href="https://github.com/NixOS/nix/blob/master/flake.nix">package</a> the Nix tool (which is mostly written in C++), thus closing the epistemological loop.</p></blockquote>
<p>The language features first-class treatment of filesystem paths and URLs (to source code and tarballs, for example).
Nix <em>expressions</em> are evaluated in a hermetic environment with no path variables and no files. All outside references get copied into content-addressed folders in the <em>store</em>, and marked as build-time <em>dependencies</em> in any <em>derivation</em> that gets defined. Retained <em>runtime</em> dependencies are calculated by scanning the actual binary for store paths.</p>
<h2 id="the-build">The Build</h2>
<p>The component is the result of <em>realizing</em> the derivation. That means grabbing all the store path contents and combining them in the prescribed way. Imagine you have 3 components that are build-time dependencies:</p>
<ol>
<li>A compiler</li>
<li>Source code</li>
<li>A bash script that uses the compiler to build the source</li>
</ol>
<p>The derivation describes where to find these, and the (obvious) way that they fit together. The result is content-addressed with a hash based on all its dependencies, and placed in the store.</p>
<h2 id="nixpkgs">nixpkgs</h2>
<p>It turns out this scheme is highly flexible and repeatable.
<a href="https://github.com/NixOS/nixpkgs">nixpkgs</a> is a repo full of Nix expressions for over 120,000 packages. You can pull any one of them into an ephemeral shell to try it out, or install it system-wide if you&rsquo;re using:</p>
<h2 id="nixos">NixOS</h2>
<p>Finally, notice that &ldquo;<em>software components</em>&rdquo; could have a very broad definition. So broad, in fact, to include an entire OS with all of the installed programs and services. That&rsquo;s what NixOS is: A Linux distro defined entirely in Nix and built around the <code>/nix/store</code> path model instead of the more usual <a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">FHS</a>.  I&rsquo;ve written <a href="/posts/first-principles-infrastructure">a bit</a> about how the Nix <em>module</em> system helps encapsulate complex system recipes into simple one-liners.</p>
<p>All my homies use NixOS. To paraphrase from <a href="https://joshblais.com/blog/nixos-is-the-endgame-of-distrohopping/">NixOS is the endgame of distrohopping </a>:</p>
<h3 id="no-more-configuration-drift---solved-">No more configuration drift - solved ✅</h3>
<h3 id="works-on-my-machine---solved-">“Works on my machine!” - solved ✅</h3>
<h3 id="dependency-hell---solved-">Dependency Hell - solved ✅</h3>
<h3 id="virtual-and-development-environments---solved-">Virtual and development environments - solved ✅</h3>
<h3 id="package-management---solved-">package management - solved ✅</h3>
<h2 id="ready-to-go-deeper">Ready to go deeper?</h2>
<p>To get started I highly recommend working through the <a href="https://nix.dev/tutorials/">tutorials at nix.dev</a>!</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>First Principles Infrastructure: Why NixOS Beats Containers and Clouds in 2025</title>
      <link>https://paradigmatic.systems/posts/first-principles-infrastructure/</link>
      <pubDate>Tue, 13 May 2025 14:30:00 +0000</pubDate>
      
      <guid>https://paradigmatic.systems/posts/first-principles-infrastructure/</guid>
      <description>Why I&amp;#39;m long-term bearish on Docker, Kubernetes, and managed clouds—and how NixOS solves dependencies and deployment from first principles.</description>
      <content:encoded><![CDATA[<p>In the marketplace of developer skills, there&rsquo;s a certain category that I&rsquo;m long-term bearish on.  They don&rsquo;t warrant investing free time trying to level up. Devs should limit their exposure to the minimum necessary to do their job. I&rsquo;m talking about tools like Docker, Kubernetes, and the intricate managed hosting platforms of Azure and AWS. These are tools that help manage</p>
<ul>
<li>social problems caused by scaling organizations to thousands of engineers</li>
<li>existing complexity</li>
<li>laziness or desire to throw money at a problem rather than solve it</li>
</ul>
<p>While they do solve real technical problems, I&rsquo;ll argue here that there&rsquo;s superior tooling for most use cases outside of big tech and legacy systems.</p>
<h1 id="containerize-or-modularize">Containerize or Modularize?</h1>
<blockquote>
<p>We need deployment consistency and environment isolation, and containers solve those!</p></blockquote>
<p>The 2016 essay <a href="https://catern.com/docker.html">Docker Considered Harmful</a> explains how Linux primitives solve the same problems in a more robust way. One valid criticism of this is that Docker can get a junior developer productive more quickly without needing to learn about arcane system settings.</p>
<p>Fine, so we need an abstraction to work with.</p>
<p>Does the abstraction need to expose us to a bloated ecosystem prone to drift? What if it could evaluate to the precise, minimal, and correct system without introducing overhead? What if it&rsquo;s a <a href="https://xeiaso.net/talks/2024/nix-docker-build/">better Docker image builder than Docker&rsquo;s image builder</a>?</p>
<p>Enter Nix with its <a href="https://nix.dev/tutorials/module-system/index.html">module system</a>. Nix approaches the &ldquo;dependency hell&rdquo; problem from first principles. It requires a radically different mental model, which is itself a social organization problem. This model, by the way, is laid out best in Eelco Dolstra&rsquo;s <a href="https://edolstra.github.io/pubs/phd-thesis.pdf">2006 PHD Thesis (PDF)</a>.</p>
<p>When you think about</p>
<ul>
<li>polyglot system with multiple language ecosystems working together</li>
<li>co-existing packages with conflicting dependencies (i.e dfferent versions of python)</li>
</ul>
<p>do you get a headache thinking about all the setup? Well then you should probably swallow some Nix pills. No other tool can encapsulate cross-ecosystem practices and Linux expertise so cleanly. A simple configuration change like</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>services<span style="color:#f92672">.</span>postgres<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span></code></pre></div><p>is the tip of a carefully crafted reproducible iceberg. This line of code hides a host of system-level changes including</p>
<ul>
<li>User and groups setup with correct isolation and permissions</li>
<li>Directory structure with ownership and permissions</li>
<li>Initialization, default template databases</li>
<li>SystemD service with correct dependencies, start order, lifecycle and recovery
and much more!</li>
</ul>
<p>It&rsquo;s a massive setup process that would normally be done manually, maybe captured in brittle shell scripts, or containerized. But here it&rsquo;s a one-liner, and it&rsquo;s exposed for use as a declarative model that allows completely deterministic and atomic system upgrades!</p>
<p>So while containers provide language-agnostic deployment, Nix achieves the same universality at the package level while maintaining better reproducibility and lower overhead. You can even spit out a Docker image as an afterthought if that&rsquo;s what you really need.</p>
<h1 id="cluster-or-cluster">Cluster or Cluster*@!#?</h1>
<blockquote>
<p>We need service discovery, load balancing, and observability! Kubernetes provides these with a vast ecosystem of battle-tested tools, and its nice declarative operator pattern for nearly every infrastructure need.</p></blockquote>
<p>The infrastructure problem is solved by creating different problems: cognitive overhead, ecosystem lock-in, long development cycles, and significant cost markup. There are compelling reasons to look for a better alternative.</p>
<p>Well, the largest package repo in the world (Nixpkgs) has nice <a href="https://search.nixos.org/options?channel=25.05&amp;from=0&amp;size=50&amp;sort=relevance&amp;type=packages">modules</a> defined for over 20,000 world-class open-source tools. With a 3-line change to your system configuration, you&rsquo;re now building a robust set of tools:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>services <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  nginx<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;      <span style="color:#75715e">#Load balancing</span>
</span></span><span style="display:flex;"><span>  consul<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;     <span style="color:#75715e">#Service discovery</span>
</span></span><span style="display:flex;"><span>  prometheus<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>; <span style="color:#75715e">#Observability</span>
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">#You&#39;ll want to configure these, of course.</span>
</span></span></code></pre></div><p>But that&rsquo;s not it. The <a href="https://nix.dev/manual/nix/2.28/command-ref/new-cli/nix3-flake.html#flake-format">Nix flake output schema</a> specifies <code>nixosConfigurations</code> as a key. Note the plural. So you might do something like:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nix" data-lang="nix"><span style="display:flex;"><span>nixosConfigurations <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>  web <span style="color:#f92672">=</span> mkNode <span style="color:#e6db74">&#34;web&#34;</span> {
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>nginx<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>nginx<span style="color:#f92672">.</span>virtualHosts<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;web&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      locations<span style="color:#f92672">.</span><span style="color:#e6db74">&#34;/api/&#34;</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>      proxyPass <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;http://</span><span style="color:#e6db74">${</span>cluster<span style="color:#f92672">.</span>db<span style="color:#f92672">.</span>ip<span style="color:#e6db74">}</span><span style="color:#e6db74">:5000/&#34;</span>;
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>    app<span style="color:#f92672">.</span>db_address <span style="color:#f92672">=</span> cluster<span style="color:#f92672">.</span>db<span style="color:#f92672">.</span>ip
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  db <span style="color:#f92672">=</span> mkNode <span style="color:#e6db74">&#34;db&#34;</span> {
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>postgresql<span style="color:#f92672">.</span>enable <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>postgresql<span style="color:#f92672">.</span>enableTCPIP <span style="color:#f92672">=</span> <span style="color:#66d9ef">true</span>;
</span></span><span style="display:flex;"><span>    services<span style="color:#f92672">.</span>postgresql<span style="color:#f92672">.</span>authentication <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;&#39;
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      host all all </span><span style="color:#e6db74">${</span>cluster<span style="color:#f92672">.</span>web<span style="color:#f92672">.</span>ip<span style="color:#e6db74">}</span><span style="color:#e6db74">/32 trust
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">      &#39;&#39;</span>;
</span></span><span style="display:flex;"><span>    api<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></code></pre></div><p>where the 2 nodes configurations make them aware of each other.</p>
<p>The same flake can even include</p>
<ul>
<li>integration tests using the insanely powerful <a href="https://nixcademy.com/posts/nixos-integration-tests/">integration test driver</a></li>
<li>deployment metadata with <a href="/posts/setting-up-deploy-rs">deploy-rs</a> to deploy the whole cluster with 1 command</li>
</ul>
<p>So the question then becomes, do you have a nest of messy legacy code and need to throw money at scaling it up? Or do you have the luxury of using the tools directly and scaling piecewise as needs arise?</p>
<h1 id="summary">Summary</h1>
<p>It&rsquo;s never going to be easy to retrofit legacy enterprise systems. But for designing new systems, the NixOS advantage is massive. Your entire system configuration, from kernel to application dependencies to infrastructure services, is a single, reproducible, version-controlled artifact. It allows for</p>
<ul>
<li>One deployment mechanism</li>
<li>Deterministic deployments</li>
<li>Exact parity between development and production</li>
</ul>
<p>and most importantly, a level of autonomy and sovereignty that you yield when your system is built on unstable abstractions.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>The Power of Nix: Developing, Building, and Deploying a Phoenix App</title>
      <link>https://paradigmatic.systems/posts/phoenix-in-nix/</link>
      <pubDate>Tue, 22 Apr 2025 04:30:00 +0000</pubDate>
      
      <guid>https://paradigmatic.systems/posts/phoenix-in-nix/</guid>
      <description>The &amp;#39;holy grail&amp;#39; Nix flake for Phoenix: pinned deps, a dev shell, a build derivation, and deployment—one piece of infrastructure for the whole lifecycle.</description>
      <content:encoded><![CDATA[<p>The inordinate usefulness of Nix really sinks in when you realize that it allows you to define a single piece of unified infrastructure for handling all the different parts of the software lifecycle. Furthermore, this infrastructure doesn&rsquo;t even need to be project-specific. For a given class of projects, the &ldquo;holy grail&rdquo; Nix flake template defines all the following:</p>
<ol>
<li>All the <strong>dependencies</strong>, pinned in the <code>flake.lock</code> and updatable with <code>nix flake update</code>.</li>
<li>A <strong>dev environment</strong> accessible with <code>nix develop</code>.</li>
<li>A <strong>package</strong> derivation that encapsulates the build process in a <code>nix build</code> command.</li>
<li>A <strong>module</strong> that exposes the app as a configurable system-level service that can be leveraged in integration checks with <code>nix flake check</code>.</li>
</ol>
<p>With these things in place, activities that traditionally create overhead for developers (environment and deployment) are abstracted and standardized. And even though other tools solve similar problems, none of them achieve the universality of the Nix model. That&rsquo;s because the most natural and complete description of software packaging is as pure functions over store paths. But in this post I&rsquo;m focusing on convenience. The correctness of Nix, and the fact that correctness leads to convenience, are topics for a different post.</p>
<p>I&rsquo;ve released some Nix infrastructure for Phoenix in a repo called <a href="https://github.com/roriholm/phoeNIX">PhoeNIX</a>. It&rsquo;s constructed, as is tradition, by copy-pasting from someone smarter (shoutout <a href="https://www.zekedou.live/">Zeke Dou</a> and <a href="https://github.com/jurraca">jurraca</a>) and then extending to meet my needs. I haven&rsquo;t seen any other flake templates for Phoenix that hit all 4 of my holy grail elements, so I hope this repo is useful for someone.</p>
<p>If you&rsquo;ve got a <a href="/posts/setting-up-deploy-rs">deploy-rs setup</a> for pushing system closures, then you can achieve a workflow that makes other DevOps, Cloud, and Containerization solutions look somewhat inadequate.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
