<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Migration | Bas Nijholt</title><link>https://www.nijho.lt/tag/migration/</link><atom:link href="https://www.nijho.lt/tag/migration/index.xml" rel="self" type="application/rss+xml"/><description>Migration</description><generator>Hugo</generator><language>en-us</language><copyright>© 2026</copyright><lastBuildDate>Thu, 23 Jul 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.nijho.lt/media/icon_hu_408a8f6cef43e327.png</url><title>Migration</title><link>https://www.nijho.lt/tag/migration/</link></image><item><title>The last btrfs machine: migrating my PC to ZFS</title><link>https://www.nijho.lt/post/btrfs-to-zfs/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/btrfs-to-zfs/</guid><description>&lt;p&gt;My GPU machine was the last machine in my fleet still running btrfs.&#10;I &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;bought it for gaming&lt;/a&gt; (which never happened), and these days its dual RTX 3090s almost exclusively run &lt;a href="https://www.nijho.lt/post/llama-nixos/"&gt;local AI models&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;btrfs itself never failed me.&#10;When I set up NixOS on this machine for the first time, I had read enough forum threads to become convinced that ZFS and NixOS were a troublesome combination, so I picked btrfs to be safe.&#10;That belief was simply wrong.&#10;As I &lt;a href="https://www.nijho.lt/post/proxmox-to-nixos/"&gt;went full Nix&lt;/a&gt; and eventually &lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;moved even my TrueNAS to NixOS&lt;/a&gt;, I learned there was no issue at all.&#10;I just had to figure out the right incantation to make ZFS work with boot drives, &lt;a href="https://github.com/nix-community/disko" target="_blank" rel="noopener"&gt;disko&lt;/a&gt;, and &lt;a href="https://github.com/nix-community/nixos-anywhere" target="_blank" rel="noopener"&gt;nixos-anywhere&lt;/a&gt;.&#10;My HP got ZFS first, that config grew into a shared disko module, and soon the whole fleet ran one pattern: NixOS on ZFS, snapshots and replication declared in code, and backups I can reason about.&lt;/p&gt;&#10;&lt;p&gt;Only the machine I sit at every day kept running on my old misconception, because a daily driver is exactly the one you do not casually reinstall.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#why-touch-a-working-machine"&gt;Why touch a working machine&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-backup-that-wasnt"&gt;The backup that wasn&amp;rsquo;t&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#what-the-audit-found"&gt;What the audit found&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-plan-one-shared-disko-module"&gt;The plan: one shared disko module&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#rehearsing-in-a-vm-again"&gt;Rehearsing in a VM, again&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-recovery-kit"&gt;The recovery kit&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#trusting-the-backup-enough-to-wipe-the-disk"&gt;Trusting the backup enough to wipe the disk&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-fifth-question"&gt;The fifth question&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#restoring-in-stages"&gt;Restoring in stages&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#slimming-down-first"&gt;Slimming down first&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-cutover"&gt;The cutover&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="why-touch-a-working-machine"&gt;Why touch a working machine &lt;a class="anchor" href="#why-touch-a-working-machine" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The operating system was already uniform.&#10;The filesystem was not, and that turned out to matter more than I expected.&lt;/p&gt;&#10;&lt;p&gt;The real trigger was that I got increasingly paranoid about an AI agent messing up my system.&#10;I run agentic AI in YOLO mode, exclusively.&#10;All day, every day, agents execute commands on this machine without asking me first.&#10;YOLO does not mean root, though: the agents run as my regular user, and the backup credentials live where only root can read them.&#10;An agent that could destroy the backup itself would make everything else in this post pointless.&#10;Meanwhile the stories keep coming in about even the most frontier models wiping someone&amp;rsquo;s home directory:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;GPT-5.6-Sol just accidentally deleted almost ALL of my Mac&amp;rsquo;s files. And this is why I trust Fable 1000x more.&lt;/p&gt;&#10;&lt;p&gt;— &lt;a href="https://x.com/mattshumer_/status/2075657271401390161" target="_blank" rel="noopener"&gt;@mattshumer_&lt;/a&gt;&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The reaction is always about which model deserves more trust.&#10;I read these tweets differently.&#10;Give any agent enough sessions and it will eventually delete something important, the same way I do every few years with a tired &lt;code&gt;rm -rf&lt;/code&gt;.&#10;Whether that moment becomes a catastrophe or an inconvenience depends entirely on the restore path.&lt;/p&gt;&#10;&lt;p&gt;So I felt I needed to properly audit the way my backups were done.&#10;I want to be able to answer four questions about any machine I own:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;What exactly is in the backup?&lt;/li&gt;&#10;&lt;li&gt;What is &lt;em&gt;not&lt;/em&gt; in the backup, and did I choose that deliberately?&lt;/li&gt;&#10;&lt;li&gt;How big is a restore?&lt;/li&gt;&#10;&lt;li&gt;How long will it take?&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="the-backup-that-wasnt"&gt;The backup that wasn&amp;rsquo;t &lt;a class="anchor" href="#the-backup-that-wasnt" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Asking &lt;span class="hover-tip" data-tooltip="What exactly is in the backup?" tabindex="0"&gt;question 1&lt;/span&gt; delivered an unpleasant answer within the hour: the backups had quietly stopped four months earlier.&#10;Two boring causes had stacked up: an interrupted prune left a stale repository lock, which the job could never clear because its own unlock step only ran &lt;em&gt;after&lt;/em&gt; a successful backup, and the repository URL still pointed at &lt;code&gt;truenas.local&lt;/code&gt;, a name that had stopped resolving &lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;along the way&lt;/a&gt;.&#10;The last successful backup ran at 07:00 on March 22; the next one at 07:16 on July 22, after &lt;a href="https://github.com/basnijholt/dotfiles/commit/4d2a75efcb503b9ca2aabcbe8339be3e5a62d0ce" target="_blank" rel="noopener"&gt;the fix&lt;/a&gt;.&#10;Four months, almost to the minute.&#10;systemd logged the failure on every single run, nobody read it, and nothing escalated.&#10;I found out because I asked the question, not because anything told me.&lt;/p&gt;&#10;&lt;h2 id="what-the-audit-found"&gt;What the audit found &lt;a class="anchor" href="#what-the-audit-found" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;On the btrfs box, &lt;span class="hover-tip" data-tooltip="How big is a restore?" tabindex="0"&gt;question 3&lt;/span&gt; alone had three different answers depending on which tool I asked.&#10;&lt;code&gt;du&lt;/code&gt; counted reflinked copies at full size, so directories reported terabytes that physically did not exist.&#10;&lt;code&gt;df&lt;/code&gt; counted physical extents, which is honest about disk usage but tells you nothing about what a file-based restore will write.&#10;And restic reported the &lt;em&gt;apparent&lt;/em&gt; size of sparse VM images, which inflated the restore estimate to several times the disk.&lt;/p&gt;&#10;&lt;p&gt;When your storage layer gives you three sizes for the same data, you do not really understand your backup.&#10;You have a backup, and you have hope.&lt;/p&gt;&#10;&lt;p&gt;&lt;span class="hover-tip" data-tooltip="How long will it take?" tabindex="0"&gt;Question 4&lt;/span&gt; was no better.&#10;Because the PC was my only btrfs machine, it could not join &lt;a href="https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/replication.nix" target="_blank" rel="noopener"&gt;the ZFS snapshot replication the rest of the fleet uses&lt;/a&gt;, so its backups relied entirely on restic pushing to the NAS.&#10;Off-machine, note, but not off-site; &lt;span class="hover-tip" data-tooltip="What is not in the backup, and did I choose that deliberately?" tabindex="0"&gt;question 2&lt;/span&gt; turned out to apply to locations too, and closing that gap is its own project.&#10;restic is file-based: every run visits the metadata of every single file to find what changed.&#10;On this machine that meant close to a hundred million files per scan, which turned out to include some 600 GB of git worktrees stuffed with virtual environments and &lt;code&gt;node_modules&lt;/code&gt;.&#10;Each run took about an hour and a half, even when almost nothing had changed.&lt;/p&gt;&#10;&lt;p&gt;That scan time dictated my backup cadence.&#10;The timer ran hourly, which in practice meant back-to-back scanning all day to move a couple hundred megabytes of real changes, and I recently &lt;a href="https://github.com/basnijholt/dotfiles/commit/4f2401de725d5201d4daca0e67505ad2e6f6691a" target="_blank" rel="noopener"&gt;dropped it to every six hours&lt;/a&gt; to stop the madness.&#10;Either way, my most recent work existed only on the machine an agent was busy poking at.&lt;/p&gt;&#10;&lt;p&gt;The scanning is also not free for the hardware.&#10;Reads are gentle on an SSD compared to writes, but hourly scans of an hour and a half each kept the drive at full duty cycle and elevated temperature for most of every day.&#10;And drives stopped being cheap: I paid $227 for &lt;a href="https://www.nijho.lt/post/homelab/"&gt;a 4 TB NVMe drive&lt;/a&gt; in 2024, and the same drive lists for around $800 today.&#10;I would rather not run the experiment of what years of continuous scanning do to a drive I can no longer afford to replace.&lt;/p&gt;&#10;&lt;p&gt;btrfs has perfectly good snapshots of its own, but sending them off-site was never an option for me: snapshot replication needs the same filesystem on the receiving end, and my NAS speaks ZFS.&#10;With ZFS I can snapshot every few minutes and &lt;code&gt;zfs send&lt;/code&gt; the actual block storage: an incremental send streams exactly the blocks that changed since the last snapshot, without walking a single file.&#10;A hundred million files cost nothing when nobody has to visit them.&lt;/p&gt;&#10;&lt;p&gt;ZFS does not magically collapse those numbers into one.&#10;But it gives me the same semantics everywhere, including on the NAS pools that hold the backups themselves.&#10;Eight of my machines already share a single ZFS disk layout, defined in &lt;a href="https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/common/disko-zfs.nix" target="_blank" rel="noopener"&gt;one shared disko module&lt;/a&gt;.&#10;The PC was the odd one out.&#10;This migration makes it nine machines with the exact same strategy, and one backup story to audit instead of one per machine.&lt;/p&gt;&#10;&lt;h2 id="the-plan-one-shared-disko-module"&gt;The plan: one shared disko module &lt;a class="anchor" href="#the-plan-one-shared-disko-module" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The Nix side was the easy part.&#10;The PC now imports the same shared disko module as the other eight machines, with one addition: a &lt;code&gt;swapSize&lt;/code&gt; parameter, because ZFS has no reliable swapfile story and the PC actually needs its 96 GB of swap.&lt;/p&gt;&#10;&lt;p&gt;The disk is &lt;a href="https://github.com/basnijholt/dotfiles/blob/8c0bc5faded40362ac01347682aaf2089562047d/configs/nixos/hosts/pc/disko.nix" target="_blank" rel="noopener"&gt;pinned by ID&lt;/a&gt;, not by device name:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/dev/disk/by-id/nvme-Samsung_SSD_990_EVO_Plus_4TB_...&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This matters more than usual here.&#10;The PC dual-boots Windows from a second NVMe drive, and I want zero ambiguity about which of the two disks disko is allowed to erase.&lt;/p&gt;&#10;&lt;p&gt;The scary property of this migration is different from the NAS one.&#10;For the NAS, the whole point was that disko must &lt;em&gt;not&lt;/em&gt; touch the data pools.&#10;For the PC, the new ZFS pool goes on the &lt;strong&gt;same disk the data currently lives on&lt;/strong&gt;.&#10;No second disk, no local rollback, no btrfs to boot back into.&#10;From the moment disko runs, the restic repository on the NAS is the only road back.&lt;/p&gt;&#10;&lt;p&gt;That sentence is the reason most of this post is about verification.&lt;/p&gt;&#10;&lt;p&gt;The road back also has to survive the same agents that motivated all of this.&#10;The PC pushes its backups over sftp as a user that owns every file in the repository, which means a compromised PC could delete or encrypt its own safety net.&#10;So the NAS &lt;a href="https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/storage.nix#L128-L143" target="_blank" rel="noopener"&gt;snapshots the repository dataset on its own side&lt;/a&gt;, where the PC&amp;rsquo;s credentials cannot reach: two days of hourly ZFS snapshots and two weeks of dailies that only root on the NAS can destroy.&lt;/p&gt;&#10;&lt;h2 id="rehearsing-in-a-vm-again"&gt;Rehearsing in a VM, again &lt;a class="anchor" href="#rehearsing-in-a-vm-again" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The &lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;NAS migration&lt;/a&gt; taught me to rehearse the destructive step in a VM, so the flake grew a &lt;a href="https://github.com/basnijholt/dotfiles/blob/8c0bc5faded40362ac01347682aaf2089562047d/configs/nixos/flake.nix#L110" target="_blank" rel="noopener"&gt;&lt;code&gt;pc-vmtest&lt;/code&gt; target&lt;/a&gt; and I ran the whole install through &lt;code&gt;nixos-anywhere --vm-test&lt;/code&gt;.&#10;Disko formatted a virtual disk, created the pool, installed the system, and booted it with swap active.&lt;/p&gt;&#10;&lt;p&gt;Passing that test proves the generated logic.&#10;It does not prove the backup, which is where the real risk lives.&lt;/p&gt;&#10;&lt;h2 id="the-recovery-kit"&gt;The recovery kit &lt;a class="anchor" href="#the-recovery-kit" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;A restore that starts with &amp;ldquo;first, log in to the dead machine and fetch the credentials&amp;rdquo; is not a restore plan.&#10;So everything a bare reinstall needs lives in a small recovery kit on two other machines in the house, permissions locked down: the restic password and SSH key that reach the repository, the staged restore script, a couple of verification scripts, and the PC&amp;rsquo;s SSH host keys.&#10;The host keys matter more than they look: the reinstalled machine comes back with its old identity, so no client complains and nothing in the fleet needs to be taught to trust a new machine.&#10;A munge key and a README round it out.&lt;/p&gt;&#10;&lt;h2 id="trusting-the-backup-enough-to-wipe-the-disk"&gt;Trusting the backup enough to wipe the disk &lt;a class="anchor" href="#trusting-the-backup-enough-to-wipe-the-disk" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My recovery story has two layers.&#10;NixOS makes the &lt;em&gt;system&lt;/em&gt; disposable: every service, driver fix, and firewall rule is in git, so no agent can leave the machine in a state I cannot rebuild.&#10;Verified backups make the &lt;em&gt;data&lt;/em&gt; recoverable.&#10;This migration is the voluntary version of the tweets above: I am about to delete all of my PC&amp;rsquo;s files on purpose.&lt;/p&gt;&#10;&lt;p&gt;I did three verifications, each one converting an assumption into a fact.&#10;An AI agent did the tedious sweeps; I reviewed the evidence.&lt;/p&gt;&#10;&lt;p&gt;The first check was completeness.&#10;We walked everything on the machine &lt;em&gt;outside&lt;/em&gt; the backup paths, until every byte was accounted for.&#10;It was all re-downloadable caches, and the largest unexplained thing turned out to be restic&amp;rsquo;s own local cache.&#10;Everything outside the backup set is now &lt;em&gt;known&lt;/em&gt; disposable instead of &lt;em&gt;assumed&lt;/em&gt; disposable.&lt;/p&gt;&#10;&lt;p&gt;The second check was the restore path.&#10;A backup you have never restored from is a hypothesis.&#10;On a different machine, using only the recovery kit and no access to the PC at all, we restored a sample from the latest snapshot and compared hashes against the live system.&#10;The hashes matched byte for byte, and even the symlinks came back pointing at the right targets.&#10;A sample proves the path works, from credentials to symlink handling; reading back every byte is the third check&amp;rsquo;s job.&#10;The machine running the rehearsal had the kit and nothing else, so recovery provably does not depend on the machine being recovered.&lt;/p&gt;&#10;&lt;p&gt;The third check is the data itself.&#10;&lt;code&gt;restic check&lt;/code&gt; validates the repository structure without reading back the actual data blobs.&#10;Before the wipe, a &lt;code&gt;--read-data&lt;/code&gt; pass ran on the NAS, where the repository sits on local disk instead of behind the network.&#10;When the backup is the only rollback, I want its bits read back at least once.&lt;/p&gt;&#10;&lt;h2 id="the-fifth-question"&gt;The fifth question &lt;a class="anchor" href="#the-fifth-question" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The outage taught me a &lt;span class="hover-tip" data-tooltip="How do I find out when any of this stops working?" tabindex="0"&gt;fifth question&lt;/span&gt;: how do I find out when any of this stops working?&#10;All three checks above are point-in-time, and verification rots.&#10;So the NAS now &lt;a href="https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/replication.nix#L263-L285" target="_blank" rel="noopener"&gt;re-verifies every hour&lt;/a&gt;, from its own side of the sftp connection, that the newest snapshot in the repository is less than a day old, and pushes an alert to my phone when it is not.&#10;That catches every failure mode on the PC side, including &amp;ldquo;the timer is simply disabled.&amp;rdquo;&#10;The watcher itself is watched: the NAS pings an &lt;a href="https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/health.nix#L252-L280" target="_blank" rel="noopener"&gt;external dead-man&amp;rsquo;s switch&lt;/a&gt; every five minutes, so if the machine holding my only road back goes dark, the alert comes from outside the house.&lt;/p&gt;&#10;&lt;p&gt;While wiring this up we discovered that months of earlier NAS alerts had never arrived, because &lt;a href="https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/health.nix#L60-L67" target="_blank" rel="noopener"&gt;ntfy silently rejects message bodies over 4 KB&lt;/a&gt;.&#10;A notification you have never received is also a hypothesis.&lt;/p&gt;&#10;&lt;h2 id="restoring-in-stages"&gt;Restoring in stages &lt;a class="anchor" href="#restoring-in-stages" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The measured restore set came out to roughly 1 TB, and 99.9% of it is &lt;code&gt;/home&lt;/code&gt;.&#10;But two directories are 64% of that terabyte: Steam (430 GB of re-downloadable games) and my local-AI model collection (217 GB of GGUF files).&lt;/p&gt;&#10;&lt;p&gt;I do not need either of those to have a working machine.&#10;So the restore script now runs in stages:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./restore_from_backup.sh stage1 &lt;span class="c1"&gt;# everything else → a fully working machine&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo ./restore_from_backup.sh stage2 &lt;span class="c1"&gt;# Steam + models, whenever&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Stage 1 restores everything except the deferred paths, plus the system identity (host keys).&#10;Stage 2 can run days later, from the rebooted system, while I am already using the machine.&lt;/p&gt;&#10;&lt;p&gt;One detail I care about: stage 1 resolves &lt;code&gt;latest&lt;/code&gt; to a concrete snapshot ID and pins it to a file.&#10;Stage 2 reads the pin.&#10;My backups run every six hours, so without this, a timer firing between the stages would silently split the restore across two different snapshots.&lt;/p&gt;&#10;&lt;p&gt;The backup itself is untouched: Steam and the models are still in every snapshot.&#10;Only the restore order changed.&lt;/p&gt;&#10;&lt;h2 id="slimming-down-first"&gt;Slimming down first &lt;a class="anchor" href="#slimming-down-first" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The PC had also quietly become an Incus host for three LXC containers, including the &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom bots&lt;/a&gt; my family talks to every day.&#10;Before the migration, those containers moved to the NAS.&#10;That shrinks both the backup and the restore, and the family bots keep running while their old host is being wiped.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;Moving the containers produced its own lesson in btrfs size confusion.&#10;One container that measured &amp;ldquo;about 250 GB&amp;rdquo; shipped over 590 GB across the network overnight, because a directory that every disk-usage probe had failed to measure turned out to hold 2.5 TB of reflink-inflated build dependencies.&#10;The rule I took away: a directory that has &lt;em&gt;never been successfully measured&lt;/em&gt; is a red alert, not a rounding error.&#10;That kind of confusion is part of why this migration exists.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="the-cutover"&gt;The cutover &lt;a class="anchor" href="#the-cutover" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The install followed the same phased &lt;code&gt;nixos-anywhere&lt;/code&gt; pattern as the NAS cutover, driven from another machine on the LAN: kexec into an installer running in RAM, stop, re-verify the target disk from inside the installer, and only then run the destructive disko/install phase.&#10;That checkpoint is the whole point: right up to the destructive phase, aborting was still cheap.&#10;The destructive part turned out to be the trivial part: from wiping the disk to booting the new system took less than twenty minutes.&lt;/p&gt;&#10;&lt;p&gt;The one surprise of the day was in the restore, and it was exactly the kind rehearsal is supposed to catch.&#10;Stage 1 would not start: the restore script runs restic as root, and a freshly installed root has no SSH identity and no known_hosts.&#10;The key it needed sat in the recovery kit inside my home directory.&#10;The rehearsal never hit this, because the rehearsal machine&amp;rsquo;s root had already talked to the NAS.&#10;A verified restore path can still depend on state you forgot you had.&lt;/p&gt;&#10;&lt;p&gt;Stage 1 restored 534 GiB across 4.5 million files in just over an hour and a half, at wire speed.&#10;The machine rebooted with its old host keys and its old &lt;code&gt;/home&lt;/code&gt;, and stage 2 started immediately.&#10;The plan said stage 2 could wait for days; it started the minute the machine was back, because once the machine works there is no reason left to wait.&#10;The deferred 648 GiB of games and models took six more hours in the background.&lt;/p&gt;&#10;&lt;p&gt;With that, the PC finally joins the fleet pattern it was locked out of: sanoid snapshots the pool every hour, so a tired &lt;code&gt;rm -rf&lt;/code&gt;, mine or an agent&amp;rsquo;s, now costs at most an hour of work and a rollback, and a daily syncoid run sends the changed blocks to the NAS.&#10;restic is on borrowed time.&#10;Its six-hour timer keeps running until I trust the new path, and then it stops: the repository stays on the NAS as the last file-level snapshots of the machine, but nothing will be added to it.&lt;/p&gt;&#10;&lt;p&gt;Aftercare was the same short list, written down instead of remembered: revert the temporary root-SSH commit the installer needed, and put the restic password back where the backup service expects it.&lt;/p&gt;&#10;&lt;p&gt;The slow work, as always, was not the cutover.&#10;The slow work was making the cutover boring.&lt;/p&gt;&#10;&lt;h2 id="references"&gt;References &lt;a class="anchor" href="#references" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/proxmox-to-nixos/"&gt;I&amp;rsquo;ve gone full Nix: Proxmox to NixOS + Incus&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/truenas-to-nixos/"&gt;Migrating from TrueNAS to NixOS&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/homelab/"&gt;My homelab&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/pull/16" target="_blank" rel="noopener"&gt;The btrfs→ZFS migration PR&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/pull/67" target="_blank" rel="noopener"&gt;The PR that closed the silent backup-monitoring failure modes&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/nix-community/disko" target="_blank" rel="noopener"&gt;disko&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/nix-community/nixos-anywhere" target="_blank" rel="noopener"&gt;nixos-anywhere&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://restic.net/" target="_blank" rel="noopener"&gt;restic&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/jimsalterjrs/sanoid" target="_blank" rel="noopener"&gt;Sanoid&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>Migrating from TrueNAS to NixOS without losing data after the build-system rug pull</title><link>https://www.nijho.lt/post/truenas-to-nixos/</link><pubDate>Sun, 28 Jun 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/truenas-to-nixos/</guid><description>&lt;p&gt;TrueNAS was the last non-NixOS machine in my home lab.&lt;/p&gt;&#10;&lt;p&gt;I kept postponing it for a simpler reason: moving something with ~100 TiB of data feels scary.&#10;My NAS holds the big ZFS pools.&#10;It has the SMB shares, NFS exports, snapshots, replication tasks, encrypted datasets, Incus state, Docker workloads, and all the boring-but-important health checks that are easy to take for granted until something fails.&lt;/p&gt;&#10;&lt;p&gt;I had already migrated most of my machines from Proxmox to NixOS with Incus, as I wrote about in &lt;a href="https://www.nijho.lt/post/proxmox-to-nixos/"&gt;I&amp;rsquo;ve gone full Nix: Proxmox to NixOS + Incus&lt;/a&gt;.&#10;That migration made me increasingly intolerant of appliance-style infrastructure.&#10;If the important state lives in a database behind a web UI, I cannot review it in a pull request.&#10;If I change something while debugging, I cannot guarantee the change made it back into the source of truth.&#10;And if I let an AI agent help me, imperative systems become even more uncomfortable: an agent can run a hundred commands, succeed, and leave behind a system that nobody can reproduce.&#10;For the NAS specifically, that basically meant I did not dare touch the machine with an AI agent at all.&lt;/p&gt;&#10;&lt;p&gt;NixOS is different.&#10;The diff is the state.&#10;If an agent changes my infrastructure, I can review the patch.&#10;If the change is wrong, I can reject it.&#10;If it works, I can commit it.&lt;/p&gt;&#10;&lt;p&gt;TrueNAS was the last holdout.&#10;It had been itching for a while.&#10;Then the &lt;a href="https://forums.truenas.com/t/clearing-the-air-on-build-scripts/64357" target="_blank" rel="noopener"&gt;build-system rug pull&lt;/a&gt; made the decision easy.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#the-itch-before-the-last-straw"&gt;The itch before the last straw&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-last-straw"&gt;The last straw&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#why-this-was-scarier-than-proxmox"&gt;Why this was scarier than Proxmox&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#giving-an-agent-the-keys-carefully"&gt;Giving an agent the keys, carefully&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-nixos-scaffold"&gt;The NixOS scaffold&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-disko-problem"&gt;The disko problem&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#rehearsing-the-dangerous-part-in-a-vm"&gt;Rehearsing the dangerous part in a VM&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-parts-that-are-not-just-storage"&gt;The parts that are not just storage&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#secrets-stay-out-of-the-repo"&gt;Secrets stay out of the repo&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#where-this-leaves-truenas"&gt;Where this leaves TrueNAS&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#the-cutover"&gt;The cutover&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#references"&gt;References&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="the-itch-before-the-last-straw"&gt;The itch before the last straw &lt;a class="anchor" href="#the-itch-before-the-last-straw" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The build-system change was not the first thing that bothered me.&#10;It was just the last straw.&lt;/p&gt;&#10;&lt;p&gt;The deeper problem was that my TrueNAS setup had slowly become complicated in exactly the way I dislike.&#10;I had clicked many buttons in the UI over time:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;SMB shares&lt;/li&gt;&#10;&lt;li&gt;NFS exports&lt;/li&gt;&#10;&lt;li&gt;snapshot tasks&lt;/li&gt;&#10;&lt;li&gt;replication tasks&lt;/li&gt;&#10;&lt;li&gt;users and groups&lt;/li&gt;&#10;&lt;li&gt;encrypted datasets&lt;/li&gt;&#10;&lt;li&gt;apps and containers&lt;/li&gt;&#10;&lt;li&gt;Incus storage&lt;/li&gt;&#10;&lt;li&gt;old Jailmaker/systemd-nspawn state&lt;/li&gt;&#10;&lt;li&gt;alerting&lt;/li&gt;&#10;&lt;li&gt;UPS settings&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;TrueNAS can export and import its configuration.&#10;That is genuinely useful.&#10;But a database export is not the same thing as a configuration I can comfortably review.&#10;When I look at a TrueNAS backup, it is not obvious to me which settings I intentionally changed, which settings are defaults, which settings are historical leftovers, and which settings only exist because I clicked something while debugging two years ago.&lt;/p&gt;&#10;&lt;p&gt;One concrete example was NFS.&#10;I needed the &lt;code&gt;crossmnt&lt;/code&gt; export option so child datasets would show up correctly inside the Docker container host, but TrueNAS did not expose that option in the UI.&#10;So I had a periodic root cron job patching &lt;code&gt;/etc/exports&lt;/code&gt; after TrueNAS generated it.&#10;It worked, but it was exactly the kind of small appliance hack that made me uneasy: partly outside the UI, partly outside the exported configuration, and very easy to forget during a migration.&#10;In NixOS, that behavior is just part of the export line.&lt;/p&gt;&#10;&lt;p&gt;That is the exact discomfort that pushed me away from Proxmox.&#10;I do not want to remember what I clicked.&#10;I want the system to remember it as code.&lt;/p&gt;&#10;&lt;p&gt;The second itch was virtualization.&#10;My TrueNAS container story had already gone through one migration.&#10;For a while I used &lt;a href="https://github.com/Jip-Hop/jailmaker" target="_blank" rel="noopener"&gt;Jailmaker&lt;/a&gt;, which is a clever script around &lt;code&gt;systemd-nspawn&lt;/code&gt; for running Linux &amp;ldquo;jails&amp;rdquo; on TrueNAS SCALE.&#10;It worked well enough for what I needed at the time: a Debian environment where I could run Docker workloads close to the data.&#10;But it also reinforced the feeling that I was building a fairly custom server on top of an appliance OS.&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://www.truenas.com/docs/scale/25.04/" target="_blank" rel="noopener"&gt;TrueNAS 25.04 &amp;ldquo;Fangtooth&amp;rdquo;&lt;/a&gt; introduced experimental Containers, named Instances in the early 25.04 releases, and the &lt;a href="https://www.truenas.com/blog/truenas-fangtooth-25-04-1/" target="_blank" rel="noopener"&gt;25.04.1 announcement&lt;/a&gt; explicitly described Instances as being based on Incus, supporting both VMs and LXCs.&#10;That was interesting to me because the older TrueNAS VM system was libvirt-based, while Incus felt like a more coherent container/VM story.&#10;Then the &lt;a href="https://forums.truenas.com/t/truenas-virtualization-plans-for-25-04-2/46236" target="_blank" rel="noopener"&gt;TrueNAS virtualization plan for 25.04.2&lt;/a&gt; explained that Fangtooth&amp;rsquo;s Instances feature caused challenges for some users and that 25.04.2 would re-enable the older Virtualization VM path alongside Instances.&#10;The &lt;a href="https://forums.truenas.com/t/truenas-25-04-2-is-now-available/49165" target="_blank" rel="noopener"&gt;25.04.2 release announcement&lt;/a&gt; made that split official: Virtual Machines came back, while VMs created through the earlier Instances feature continued to appear under Containers.&lt;/p&gt;&#10;&lt;p&gt;For many people that churn was probably annoying.&#10;For me it was especially ironic.&#10;I had migrated from Jailmaker&amp;rsquo;s &lt;code&gt;systemd-nspawn&lt;/code&gt; setup to Incus, read up on &lt;a href="https://linuxcontainers.org/incus/" target="_blank" rel="noopener"&gt;Incus&lt;/a&gt;, got excited about it, started using it, and then became genuinely happy with it when I migrated from Proxmox to NixOS with Incus.&#10;Incus fit my mental model perfectly: a clean CLI, containers and VMs in one place, easy scripting, and a workflow that agents can inspect.&lt;/p&gt;&#10;&lt;p&gt;Then TrueNAS started moving away from that direction again.&#10;Forum discussion around the 25.04.2 and later virtualization plans made it clear that Incus was not the stable long-term abstraction I hoped it would be inside TrueNAS.&#10;In the long-running &lt;a href="https://forums.truenas.com/t/linux-jails-containers-vms-with-incus/23599?page=21" target="_blank" rel="noopener"&gt;Linux Jails with Incus thread&lt;/a&gt;, Kris Moore is quoted saying that Incus is being removed from the base system and that libvirt will handle the backend for VMs and LXC.&#10;That probably makes sense for TrueNAS.&#10;They have enterprise users, compatibility concerns, and a support matrix that is very different from mine.&lt;/p&gt;&#10;&lt;p&gt;But for me it meant the NAS was again becoming an appliance whose internal direction I had to follow.&#10;I had just escaped that with Proxmox.&#10;I did not want to do it again for the most important machine in my house.&lt;/p&gt;&#10;&lt;h2 id="the-last-straw"&gt;The last straw &lt;a class="anchor" href="#the-last-straw" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Then I saw Jeff Geerling mention TrueNAS&amp;rsquo;s recent &amp;ldquo;shady behavior&amp;rdquo; in a video, and I started looking into what had actually changed.&lt;/p&gt;&#10;&lt;p&gt;The short version is that iXsystems made part of the TrueNAS ecosystem closed source.&#10;Specifically, iXsystems moved the TrueNAS build scripts and related build infrastructure internal for the TrueNAS 27 line.&#10;Their forum post, &lt;a href="https://forums.truenas.com/t/clearing-the-air-on-build-scripts/64357" target="_blank" rel="noopener"&gt;Clearing the Air on Build Scripts&lt;/a&gt;, frames this as a practical build-system decision and says normal installation and updates remain unaffected.&#10;The old &lt;a href="https://github.com/truenas/scale-build" target="_blank" rel="noopener"&gt;&lt;code&gt;truenas/scale-build&lt;/code&gt;&lt;/a&gt; repository now says it is preserved historically and is no longer the maintained build system.&#10;Community reactions, for example &lt;a href="https://forums.truenas.com/t/truenas-closing-its-build-system-has-sparked-a-significant-trust-crisis-in-the-self-hosted-and-engineering-communities/64549" target="_blank" rel="noopener"&gt;this TrueNAS forum thread&lt;/a&gt;, understandably read this as a meaningful step away from fully open development.&lt;/p&gt;&#10;&lt;p&gt;Would I ever have built TrueNAS from source myself?&#10;Almost certainly not.&lt;/p&gt;&#10;&lt;p&gt;That misses the point.&#10;I also rarely compile my Linux kernel manually, yet I care deeply that the ecosystem is open enough that I &lt;em&gt;could&lt;/em&gt; understand, rebuild, inspect, and fork the pieces if necessary.&#10;For a storage appliance, trust matters more than convenience.&#10;The NAS is not a toy VM.&#10;It is the machine that owns my data.&lt;/p&gt;&#10;&lt;p&gt;That was the final push.&#10;The UI state bothered me.&#10;The virtualization churn bothered me.&#10;The build-system change made the decision obvious.&lt;/p&gt;&#10;&lt;h2 id="why-this-was-scarier-than-proxmox"&gt;Why this was scarier than Proxmox &lt;a class="anchor" href="#why-this-was-scarier-than-proxmox" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Migrating Proxmox to NixOS was scary in the normal infrastructure sense.&#10;I had containers and VMs to move, but the blast radius was understandable.&#10;I could export with &lt;code&gt;vzdump&lt;/code&gt;, import into Incus, and test a lot of it in virtual machines.&lt;/p&gt;&#10;&lt;p&gt;Migrating TrueNAS is a different kind of scary.&#10;The data is not a VM disk that I can casually copy around.&#10;The important thing is &lt;em&gt;not&lt;/em&gt; to recreate the data pools.&#10;The important thing is to &lt;strong&gt;not touch them&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;The target architecture is simple in principle:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nix" data-lang="nix"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Conceptually:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - disko owns only the boot disk&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# - existing data pools are imported by name&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;boot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zfs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extraPools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;ssd&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;tank&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The NixOS boot disk can be destroyed and recreated.&#10;The existing &lt;code&gt;tank&lt;/code&gt; and &lt;code&gt;ssd&lt;/code&gt; pools cannot.&lt;/p&gt;&#10;&lt;p&gt;This is the part where many migration guides become hand-wavy:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Install the new OS.&lt;/li&gt;&#10;&lt;li&gt;Import your ZFS pools.&lt;/li&gt;&#10;&lt;li&gt;Good luck.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;That is not enough for me.&#10;I could physically access the machine, but unplugging the relevant NVMe hardware would mean taking apart more of the box than I wanted to touch.&#10;So I treated the cutover as if the disks had to stay connected.&#10;And I absolutely did not want one typo in a disk path to become yet another meme in AI agent failures.&lt;/p&gt;&#10;&lt;h2 id="giving-an-agent-the-keys-carefully"&gt;Giving an agent the keys, carefully &lt;a class="anchor" href="#giving-an-agent-the-keys-carefully" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Without AI this would probably have become a week-long project that I would forever postpone.&#10;With AI, it felt plausible to get the first serious version done in a single sitting.&#10;My approach was to give GPT 5.5 in xhigh mode SSH access to the live TrueNAS box and tell it, repeatedly and very explicitly, not to change anything.&#10;To be clear, it had root access.&#10;At this point these models have become powerful enough that I trust them to follow explicit instructions.&#10;The safety boundary was instruction-following, narrow scope, and review.&lt;/p&gt;&#10;&lt;p&gt;Not &amp;ldquo;please be careful.&amp;rdquo;&#10;More like:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;You can inspect.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;You cannot change anything.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Do not start or stop services.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Do not import or export pools.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Do not load keys.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Do not mount datasets.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Read only.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is the part where people either nod along or think I have completely lost my mind.&#10;I think the distinction matters.&#10;I did not ask an agent to &amp;ldquo;migrate my NAS.&amp;rdquo;&#10;I asked it to inspect the current system, summarize the live state, compare that against my NixOS repository, and propose declarative config.&lt;/p&gt;&#10;&lt;p&gt;Then I reviewed the diffs.&#10;Then I asked for another review.&#10;Then another.&#10;The process was messier than one neat two-agent pass: build a scaffold, ask a model to attack it, compare that against the live TrueNAS state, refine the scaffold, and repeat.&#10;GPT 5.5 did most of the initial construction.&#10;Claude Opus 4.8 on max effort did one of the later independent cross-checks against the live system.&#10;By the end, I trusted the result because multiple independent reviews had converged on the same assumptions, and I understood the remaining differences.&#10;The loop felt like code review: inspect live state, turn the desired parts into Nix, review the diff, and ask another model to attack it.&#10;The scariest assumption got a VM test, and the destructive step stayed small.&lt;/p&gt;&#10;&lt;p&gt;Byte-for-byte TrueNAS cloning was never the target.&#10;I wanted every important behavior explicit: storage import, shares, snapshots, replication, Incus recovery, monitoring, and the few places where NixOS intentionally differs from the old appliance.&#10;At that point, the NixOS config reproduced the parts of TrueNAS I actually depended on, and the remaining differences were deliberate.&lt;/p&gt;&#10;&lt;p&gt;The agent was excellent at inventory:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Which pools exist&lt;/li&gt;&#10;&lt;li&gt;Which datasets are encrypted&lt;/li&gt;&#10;&lt;li&gt;Which NFS exports are live&lt;/li&gt;&#10;&lt;li&gt;Which SMB shares exist&lt;/li&gt;&#10;&lt;li&gt;Which users and numeric IDs matter&lt;/li&gt;&#10;&lt;li&gt;What Incus storage pool is in use&lt;/li&gt;&#10;&lt;li&gt;What replication tasks exist&lt;/li&gt;&#10;&lt;li&gt;What SMART/NUT/health monitoring exists&lt;/li&gt;&#10;&lt;li&gt;What host ID ZFS expects&lt;/li&gt;&#10;&lt;li&gt;Which disk is the current boot pool&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Preserving everything blindly would have been wrong.&#10;The useful parts became code, and the dangerous parts became explicit.&lt;/p&gt;&#10;&lt;h2 id="the-nixos-scaffold"&gt;The NixOS scaffold &lt;a class="anchor" href="#the-nixos-scaffold" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The resulting NixOS host is split into small files:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hosts/nas/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── default.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── disko.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── hardware-configuration.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── health.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── identity.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── networking.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── nfs.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── replication.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── samba.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── storage.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── virtualization.nix&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── CUTOVER.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── PLANNING.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is exactly why I like NixOS.&#10;The migration stopped being a list of steps I needed to follow and became a &lt;a href="https://github.com/basnijholt/dotfiles/pull/61/" target="_blank" rel="noopener"&gt;pull request&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;The important storage boundary is:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;disko.nix&lt;/code&gt; describes only the boot disk.&lt;/li&gt;&#10;&lt;li&gt;The data pools are imported by name.&lt;/li&gt;&#10;&lt;li&gt;ZFS force-import is disabled.&lt;/li&gt;&#10;&lt;li&gt;Boot-time encrypted credential prompts are disabled.&lt;/li&gt;&#10;&lt;li&gt;Encrypted datasets are unlocked manually after boot.&lt;/li&gt;&#10;&lt;li&gt;Sanoid/Syncoid replace TrueNAS snapshot and replication tasks.&lt;/li&gt;&#10;&lt;li&gt;SMB, NFS, Docker, Incus, SMART, ZED, NUT, Netdata, and exporters are all declared in code.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The data pools are intentionally &lt;em&gt;not&lt;/em&gt; in disko.&#10;That is the entire point.&lt;/p&gt;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;The host is called &lt;code&gt;nas&lt;/code&gt; because that is the role I want to keep.&#10;TrueNAS was the old implementation.&#10;NixOS becomes the new one, while preserving the services and data I actually need.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="the-disko-problem"&gt;The disko problem &lt;a class="anchor" href="#the-disko-problem" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/nix-community/disko" target="_blank" rel="noopener"&gt;&lt;code&gt;disko&lt;/code&gt;&lt;/a&gt; is a fantastic tool.&#10;It lets you describe disks declaratively and then formats/partitions/mounts them.&#10;For a fresh NixOS install, it is exactly what I want.&lt;/p&gt;&#10;&lt;p&gt;This also fits how I install NixOS machines.&#10;I build a custom &lt;a href="https://github.com/basnijholt/dotfiles/blob/main/configs/nixos/installers/iso.nix" target="_blank" rel="noopener"&gt;installer ISO&lt;/a&gt;, put it on a USB stick, and boot from it.&#10;The image already has SSH enabled with my keys, flakes enabled, and the tooling needed to run the repo-defined &lt;code&gt;disko&lt;/code&gt; and &lt;code&gt;nixos-install&lt;/code&gt; flow.&#10;That turns the installer environment into part of the reviewed configuration, instead of another place where I have to remember which choices to make.&lt;/p&gt;&#10;&lt;p&gt;It is also exactly the tool that can ruin your day if pointed at the wrong disk.&lt;/p&gt;&#10;&lt;p&gt;My requirement was:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Use disko to destroy and recreate the boot disk, but prove it cannot touch the existing ZFS data pools.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;The static config already looked right.&#10;The generated disko script only referenced the boot device and generated partition labels.&#10;The data pools were only present under &lt;code&gt;boot.zfs.extraPools&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;But &amp;ldquo;looks right&amp;rdquo; is not enough when the downside is wiping a NAS.&lt;/p&gt;&#10;&lt;p&gt;So we built a VM rehearsal.&lt;/p&gt;&#10;&lt;h2 id="rehearsing-the-dangerous-part-in-a-vm"&gt;Rehearsing the dangerous part in a VM &lt;a class="anchor" href="#rehearsing-the-dangerous-part-in-a-vm" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The VM test creates a miniature version of the dangerous situation:&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Attach several throwaway disks to a disposable NixOS VM.&lt;/li&gt;&#10;&lt;li&gt;Create a fake &lt;code&gt;boot-pool&lt;/code&gt; on one disk.&lt;/li&gt;&#10;&lt;li&gt;Create fake &lt;code&gt;tank&lt;/code&gt; and &lt;code&gt;ssd&lt;/code&gt; ZFS pools on other disks.&lt;/li&gt;&#10;&lt;li&gt;Write sentinel files into the fake data pools.&lt;/li&gt;&#10;&lt;li&gt;Export the fake data pools.&lt;/li&gt;&#10;&lt;li&gt;Run the generated &lt;code&gt;nas&lt;/code&gt; disko script against the fake boot disk.&lt;/li&gt;&#10;&lt;li&gt;Confirm the boot disk now contains the new &lt;code&gt;zroot&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Confirm fake &lt;code&gt;tank&lt;/code&gt; and &lt;code&gt;ssd&lt;/code&gt; still have their ZFS labels.&lt;/li&gt;&#10;&lt;li&gt;Re-import fake &lt;code&gt;tank&lt;/code&gt; and &lt;code&gt;ssd&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Read the sentinel files.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;The check lives in the flake now:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; configs/nixos&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nix build .#checks.x86_64-linux.nas-disko-safety&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It passed.&lt;/p&gt;&#10;&lt;p&gt;Passing that VM test gives me confidence in the generated Nix/disko logic.&#10;The read-only check on the live TrueNAS box gives me confidence in the configured disk target: the by-id path resolves to the 500 GB &lt;code&gt;boot-pool&lt;/code&gt; disk, and &lt;code&gt;tank&lt;/code&gt; and &lt;code&gt;ssd&lt;/code&gt; live on different devices.&#10;The installer preflight repeats that same check immediately before the destructive step.&lt;/p&gt;&#10;&lt;p&gt;This is the level of paranoia I want around storage migrations.&lt;/p&gt;&#10;&lt;h2 id="the-parts-that-are-not-just-storage"&gt;The parts that are not just storage &lt;a class="anchor" href="#the-parts-that-are-not-just-storage" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Once the &amp;ldquo;do not wipe the pools&amp;rdquo; boundary was in place, the rest of the migration became more normal infrastructure work.&#10;Still annoying, but normal.&lt;/p&gt;&#10;&lt;p&gt;SMB was not a byte-for-byte port.&#10;TrueNAS ships private Samba VFS modules and has its own ACL/middleware behavior.&#10;Stock NixOS Samba covers the parts I need with &lt;code&gt;fruit&lt;/code&gt;, &lt;code&gt;streams_xattr&lt;/code&gt;, and sensible share definitions, but it is not literally the same stack.&#10;So the plan calls out what must be tested from real clients: Time Machine, guest access, and photo/media access.&lt;/p&gt;&#10;&lt;p&gt;Snapshots and replication also changed.&#10;TrueNAS snapshot tasks become Sanoid policy.&#10;TrueNAS replication tasks become Syncoid systemd timers.&#10;Existing snapshots are preserved, but Sanoid will not magically prune old TrueNAS-named snapshots.&#10;That is a feature, not a bug.&#10;I would rather manually decide when old snapshots age out than have a new tool silently &amp;ldquo;clean up&amp;rdquo; history.&lt;/p&gt;&#10;&lt;p&gt;Incus is another subtle one.&#10;The storage pool lives on a data pool, so disko will not touch it.&#10;But a fresh NixOS install has a fresh Incus database.&#10;The plan is to run &lt;code&gt;incus admin recover&lt;/code&gt;, then apply a small declarative reconciler for the known instances.&#10;The state boundary has to be clear: preserve the storage dataset, recover the Incus database, then reconcile the instance config.&lt;/p&gt;&#10;&lt;p&gt;Health monitoring is also declared:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;SMARTD&lt;/li&gt;&#10;&lt;li&gt;ZFS Event Daemon&lt;/li&gt;&#10;&lt;li&gt;NUT/UPS client&lt;/li&gt;&#10;&lt;li&gt;Netdata bound locally&lt;/li&gt;&#10;&lt;li&gt;Prometheus exporters&lt;/li&gt;&#10;&lt;li&gt;A shared alert hook that can log, wall, and optionally send ntfy alerts&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;TrueNAS gives you many of these things in the UI.&#10;NixOS gives them to me as code.&lt;/p&gt;&#10;&lt;p&gt;I know using your NAS for other services is a bit of an anti-pattern.&#10;In an ideal world, the storage box just stores data and stays boring.&#10;But when I originally bought and configured this machine, it became the most powerful machine in my home lab.&#10;So it naturally started attracting the Docker workloads too.&#10;Most of those workloads are not resource intensive, and keeping them close to the data is genuinely convenient.&#10;At one point I was running more than a hundred containers on that machine and it barely made a dent.&#10;They ran inside LXC containers on Incus rather than directly on the TrueNAS host, which made it feel like a reasonable compromise for a long time.&lt;/p&gt;&#10;&lt;p&gt;The migration also became a chance to fix a real operational problem.&#10;My worst TrueNAS incident was a long OOM death spiral: no swap, too many unbounded containers, and services repeatedly getting killed and restarted.&#10;The philosophy came from Nix and open infrastructure.&#10;The OOM incident was the practical reminder that the NAS had become a general-purpose compute host.&#10;In the NixOS version, container memory limits, zram, earlyoom, and an explicit ZFS ARC cap are part of the configuration instead of being tribal knowledge or post-incident notes.&lt;/p&gt;&#10;&lt;h2 id="secrets-stay-out-of-the-repo"&gt;Secrets stay out of the repo &lt;a class="anchor" href="#secrets-stay-out-of-the-repo" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My dotfiles are public.&#10;That makes the migration more interesting.&lt;/p&gt;&#10;&lt;p&gt;The Nix config necessarily contains some operational facts: hostnames, share names, service definitions, and so on.&#10;But the plan deliberately keeps secrets and sensitive cutover material out of git.&lt;/p&gt;&#10;&lt;p&gt;For this migration, that means:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;ZFS dataset passphrases and unlock material stay off-box and outside the repo.&lt;/li&gt;&#10;&lt;li&gt;Replication private keys are installed manually under &lt;code&gt;/etc/ssh&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;Inbound authorized keys for backup senders are installed during cutover.&lt;/li&gt;&#10;&lt;li&gt;Alerting secrets live in a runtime env file.&lt;/li&gt;&#10;&lt;li&gt;A local &lt;code&gt;~/nas-cutover/&lt;/code&gt; staging directory can exist, but it is explicitly not part of the repo and should not be casually read by agents.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;One important detail: the encrypted datasets use passphrase keys.&#10;In the TrueNAS setup I handled that with &lt;a href="https://github.com/basnijholt/truenas-unlock" target="_blank" rel="noopener"&gt;truenas-unlock&lt;/a&gt;, a small tool I wrote for this exact problem.&#10;The unlock material lives on another device; when that device is on the same network, it can unlock the datasets through the TrueNAS API.&#10;That gave me a lightweight hardware/network-presence factor: the NAS could boot, and the encrypted datasets only unlocked when the separate unlock device was present too.&lt;/p&gt;&#10;&lt;p&gt;That changes the migration footgun.&#10;The TrueNAS API path does not survive the cutover as-is, so I split the same idea out into &lt;a href="https://github.com/basnijholt/zfs-unlock" target="_blank" rel="noopener"&gt;zfs-unlock&lt;/a&gt;: the NixOS/OpenZFS version that talks to a restricted SSH receiver instead.&#10;During the migration I still kept an independent manual passphrase recovery path, because I do not want the automatic unlock mechanism to be the only way back into my data.&lt;/p&gt;&#10;&lt;h2 id="where-this-leaves-truenas"&gt;Where this leaves TrueNAS &lt;a class="anchor" href="#where-this-leaves-truenas" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I still like TrueNAS.&#10;It got me very far.&#10;It made ZFS approachable.&#10;It gave me a good NAS UI when I wanted a NAS UI.&#10;And if you want an appliance that mostly owns storage for you, it is still a very compelling project.&lt;/p&gt;&#10;&lt;p&gt;I like NixOS more for my own infrastructure.&#10;The most important machine in my house should live in reviewable code instead of mutable appliance state spread across a UI, a database, and old debugging decisions.&#10;NixOS and AI are a particularly good match for that.&#10;I can describe messy infrastructure intent in imprecise English, let the agent turn that into exact Nix code, and review the diff before it becomes system state.&#10;The prompt stays fuzzy.&#10;The machine state does not.&lt;/p&gt;&#10;&lt;p&gt;After the Proxmox migration, TrueNAS was the last major exception.&#10;This migration removes that exception.&lt;/p&gt;&#10;&lt;h2 id="the-cutover"&gt;The cutover &lt;a class="anchor" href="#the-cutover" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I ended up using a phased &lt;a href="https://github.com/nix-community/nixos-anywhere" target="_blank" rel="noopener"&gt;&lt;code&gt;nixos-anywhere&lt;/code&gt;&lt;/a&gt; cutover instead of the USB installer path.&#10;The important safety property stayed the same: first kexec into a temporary NixOS installer, stop there, run the disk preflight from inside that installer, and only then run the destructive &lt;code&gt;disko,install,reboot&lt;/code&gt; phase.&#10;Before the destructive phase, the box was running the installer in RAM and the disks had not been intentionally modified yet.&#10;That gave me a real checkpoint where aborting was still cheap.&lt;/p&gt;&#10;&lt;p&gt;I kept my hands on the few commands that actually changed the machine.&#10;I ran the &lt;code&gt;nixos-anywhere&lt;/code&gt; phases and the later &lt;code&gt;nixos-rebuild&lt;/code&gt; commands myself.&#10;The agent stayed next to it, reading the outputs, checking the assumptions, SSHing in to verify pool health, services, mounts, Incus state, NFS clients, and then turning any findings back into code or documentation.&#10;That split felt right to me: I pressed enter on the small number of irreversible commands, and the AI did the tedious verification work that I would otherwise be tempted to rush.&lt;/p&gt;&#10;&lt;p&gt;The actual migration was almost anticlimactic compared to the preparation.&#10;From the moment TrueNAS stopped being the running OS to having the Incus containers back with my hundred-plus services running again took less than 20 minutes.&#10;The slow work was not the cutover.&#10;The slow work was making the cutover boring.&lt;/p&gt;&#10;&lt;details&gt;&#10;&lt;summary&gt;The small things that still needed fixing during cutover&lt;/summary&gt;&#10;&lt;p&gt;There were a few small first-boot reconciliations. I am writing them down mostly for future readers who want to follow a similar path:&lt;/p&gt;&#10;&lt;ul&gt;&#10; &lt;li&gt;imported data-pool mountpoints needed to be reconciled to the &lt;code&gt;/mnt/...&lt;/code&gt; paths my clients and services already used;&lt;/li&gt;&#10; &lt;li&gt;encrypted datasets needed a better manual post-boot unlock helper, and the old TrueNAS API unlock flow needed a NixOS/OpenZFS successor;&lt;/li&gt;&#10; &lt;li&gt;Incus needed &lt;code&gt;incus admin recover&lt;/code&gt; because the storage pool survived but the new OS had a fresh Incus database;&lt;/li&gt;&#10; &lt;li&gt;one unprivileged recovered container needed the matching subordinate UID/GID ranges declared for its passthrough idmap;&lt;/li&gt;&#10; &lt;li&gt;&lt;code&gt;nas.local&lt;/code&gt; needed an explicit DNS record so my wildcard did not send it to a workload container;&lt;/li&gt;&#10; &lt;li&gt;the old PC job that backed up the TrueNAS API config needed to be removed;&lt;/li&gt;&#10; &lt;li&gt;the PC NFS mounts, Nix cache endpoints, and recovered Docker workload container all needed client-side validation.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Those findings went back into the cutover runbooks and the Nix config in a &lt;a href="https://github.com/basnijholt/dotfiles/pull/62"&gt;follow-up PR&lt;/a&gt;, so the notes are available for anyone who might benefit from them later.&lt;/p&gt;&#10;&lt;/details&gt;&#10;&lt;p&gt;At the end of the cutover, the machine was &lt;code&gt;nas&lt;/code&gt;, the ZFS pools were healthy, the NFS mounts worked from my PC, the Incus containers were recovered, and the Docker workloads were running again.&#10;SMB authentication, guest access, Time Machine, and normal media browsing from macOS Finder worked after one final share-root permission fix.&#10;At that point the migration was working end to end: the pools were healthy and the services I actually use were back.&lt;/p&gt;&#10;&lt;h2 id="references"&gt;References &lt;a class="anchor" href="#references" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/proxmox-to-nixos/"&gt;I&amp;rsquo;ve gone full Nix: Proxmox to NixOS + Incus&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/homelab/"&gt;My homelab: from a single Raspberry Pi to a Proxmox cluster and a dedicated NAS&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;On agentic coding&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://forums.truenas.com/t/clearing-the-air-on-build-scripts/64357" target="_blank" rel="noopener"&gt;TrueNAS forum: Clearing the Air on Build Scripts&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/truenas/scale-build" target="_blank" rel="noopener"&gt;&lt;code&gt;truenas/scale-build&lt;/code&gt; on GitHub&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://forums.truenas.com/t/truenas-closing-its-build-system-has-sparked-a-significant-trust-crisis-in-the-self-hosted-and-engineering-communities/64549" target="_blank" rel="noopener"&gt;TrueNAS forum: build-system trust crisis discussion&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.truenas.com/docs/scale/25.04/" target="_blank" rel="noopener"&gt;TrueNAS 25.04 documentation&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.truenas.com/blog/truenas-fangtooth-25-04-1/" target="_blank" rel="noopener"&gt;TrueNAS 25.04.1: Fangtooth Unification Gains Momentum&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://forums.truenas.com/t/truenas-virtualization-plans-for-25-04-2/46236" target="_blank" rel="noopener"&gt;TrueNAS Virtualization Plans for 25.04.2&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://forums.truenas.com/t/truenas-25-04-2-is-now-available/49165" target="_blank" rel="noopener"&gt;TrueNAS 25.04.2 release announcement&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://forums.truenas.com/t/linux-jails-containers-vms-with-incus/23599?page=21" target="_blank" rel="noopener"&gt;Linux Jails with Incus forum thread&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/Jip-Hop/jailmaker" target="_blank" rel="noopener"&gt;Jailmaker&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/truenas-unlock" target="_blank" rel="noopener"&gt;truenas-unlock&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/zfs-unlock" target="_blank" rel="noopener"&gt;zfs-unlock&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/pull/61" target="_blank" rel="noopener"&gt;The NAS NixOS scaffold PR&lt;/a&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item><item><title>I've gone full Nix: Proxmox to NixOS + Incus</title><link>https://www.nijho.lt/post/proxmox-to-nixos/</link><pubDate>Sat, 29 Nov 2025 13:00:00 +0100</pubDate><guid isPermaLink="false">/post/proxmox-to-nixos/</guid><description>&lt;h1 id="ive-gone-full-nix-proxmox-to-nixos--incus"&gt;I&amp;rsquo;ve gone full Nix: Proxmox to NixOS + Incus&lt;/h1&gt;&#10;&lt;p&gt;I have officially decommissioned my Proxmox cluster.&#10;After years of running my homelab on Proxmox, starting with a single NUC and expanding to a multi-node cluster, I have migrated everything to NixOS running Incus.&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;strong&gt;Update 2026-06-26:&lt;/strong&gt; This post reached the front page of &lt;a href="https://news.ycombinator.com/item?id=48679385" target="_blank" rel="noopener"&gt;Hacker News&lt;/a&gt;, and several readers correctly pointed out that Proxmox is not limited to clicking around in the web UI.&#10;It has &lt;code&gt;qm&lt;/code&gt;, &lt;code&gt;pct&lt;/code&gt;, config files, a REST API, Terraform providers, and Ansible workflows.&#10;My point is not that Proxmox cannot be automated.&#10;Even with that automation, state drift can still creep in when debugging means running one quick command, especially if an agent is allowed to execute imperative fixes that never make it back into the automation framework.&#10;It is that, for my setup, I wanted the reproducible configuration itself to be the source of truth.&#10;The thing I care about is not buttons versus commands, but whether I can rebuild the host from version-controlled text files and know that every important change is captured there.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="from-skeptic-to-believer"&gt;From Skeptic to Believer &lt;a class="anchor" href="#from-skeptic-to-believer" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I wasn&amp;rsquo;t always a Nix evangelist.&#10;In fact, I initially despised the language and its syntax.&#10;I couldn&amp;rsquo;t figure out how it worked, and I already had my own specific way of setting up my dotfiles.&#10;I used &lt;a href="https://www.nijho.lt/post/dotfiles/"&gt;Dotbot&lt;/a&gt; for symlinking and a tool I wrote called &lt;a href="https://www.nijho.lt/post/dotbins/"&gt;dotbins&lt;/a&gt; for managing binaries.&#10;I didn&amp;rsquo;t feel like I required Nix for most of my tools.&#10;I used &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nix-darwin/configuration.nix" target="_blank" rel="noopener"&gt;nix-darwin&lt;/a&gt; on my Mac for a long time, but only to specify Homebrew packages and application settings.&lt;/p&gt;&#10;&lt;p&gt;My true conversion happened when I bought my gaming PC, as described in my &lt;a href="https://www.nijho.lt/post/local-ai-journey/"&gt;local LLM post&lt;/a&gt;.&#10;I initially installed Pop!_OS because I wanted to play games and absolutely wanted to avoid Windows.&#10;I got some games to work, but I constantly ran into NVIDIA driver issues that required running random, imperative commands to fix.&#10;I felt that was a bad solution because I could never reproduce those debugging steps later.&#10;Then I did the dumb thing of updating my NVIDIA drivers, not realizing that imperatively managing driver versions and repositories is a recipe for disaster, and got stuck in a GRUB boot loop.&#10;Frustrated, I installed NixOS, hoping its promise of atomic updates would solve this.&#10;The result was glorious.&#10;I never really believed that everything would be byte-for-byte equivalent until I migrated that system to a new disk.&#10;I didn&amp;rsquo;t clone the drive; I just applied my Nix configuration to a fresh install.&#10;It booted, I copied my data, and everything was identical.&#10;That was the moment it clicked.&lt;/p&gt;&#10;&lt;h2 id="the-friction-of-imperative-systems"&gt;The Friction of Imperative Systems &lt;a class="anchor" href="#the-friction-of-imperative-systems" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Proxmox is fantastic software.&#10;It lowered the barrier to entry for me and taught me almost everything I know about virtualization, LXC containers, and ZFS.&#10;But fundamentally, Proxmox is built around &lt;strong&gt;clicking buttons&lt;/strong&gt;.&#10;It is a GUI-first paradigm.&#10;While you &lt;em&gt;can&lt;/em&gt; automate it with Terraform or Ansible, it often feels like fighting the tool.&#10;State drift is real.&#10;You change a setting in the UI to debug something, forget about it, and six months later your &amp;ldquo;infrastructure as code&amp;rdquo; is out of sync with reality.&#10;For a human maintaining a system manually, this is annoying.&#10;But when you introduce AI agents, this becomes a disaster for the operator.&#10;An agent running in &amp;ldquo;YOLO mode&amp;rdquo; might execute hundreds of imperative commands to fix a problem.&#10;It might succeed, but it leaves your system in an undefined, unreproducible state that no one—not even the agent—can fully understand or replicate later.&lt;/p&gt;&#10;&lt;p&gt;This friction manifests in hardware management too.&#10;On my HP EliteDesk, the Intel I219-LM network card has a known bug where it hangs with hardware offloading enabled.&#10;I vaguely remembered fixing this years ago on Proxmox, but I had forgotten the details.&#10;When I set up NixOS, I ran into the same issue: the network would randomly drop.&#10;This time, however, the fix isn&amp;rsquo;t a forgotten command run in a root shell history.&#10;It is a &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nixos/hosts/hp/networking.nix#L16-L31" target="_blank" rel="noopener"&gt;documented systemd service&lt;/a&gt; in my configuration.&#10;I added a comment explaining exactly &lt;em&gt;why&lt;/em&gt; &lt;code&gt;tso off gso off&lt;/code&gt; is needed, citing the forum threads.&#10;If I ever reinstall this machine, the fix applies automatically.&#10;On Proxmox, I would have had to rediscover this pain all over again.&lt;/p&gt;&#10;&lt;p&gt;Another example is my Intel NUC.&#10;Since my homelab lives behind my TV, I wondered if I could also use it as a Home Theater PC (HTPC).&#10;On Proxmox, this would have required passing the GPU through to a VM to get video output.&#10;But doing so would mean the Proxmox host loses access to the GPU entirely, meaning no local console if things go wrong.&#10;It was a strict trade-off: either a media player, or a debuggable hypervisor. I tried it, but it was so much trouble that I quickly reverted.&#10;With NixOS, I don&amp;rsquo;t have to choose.&#10;The host OS runs &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nixos/hosts/nuc/kodi.nix" target="_blank" rel="noopener"&gt;Kodi directly&lt;/a&gt;, giving me native hardware acceleration and video output.&#10;Simultaneously, &lt;code&gt;incus&lt;/code&gt; runs in the background, hosting my containers.&#10;I get my HTPC and my server on the same metal, without the virtualization tax or the &amp;ldquo;headless host&amp;rdquo; limitation.&lt;/p&gt;&#10;&lt;p&gt;There is a deeper philosophical difference too.&#10;Systems like Proxmox or TrueNAS are designed as appliances.&#10;You aren&amp;rsquo;t supposed to run arbitrary commands on the host; installing packages or tweaking config files is discouraged because you might break the middleware or lose changes on upgrade.&#10;You are effectively locked out of your own hardware&amp;rsquo;s full potential.&#10;With NixOS, the host is fully mine.&#10;I can mess with it—installing Kodi, tweaking network drivers, running local LLMs—without fear.&#10;Because the state is declarative, it is 100% obvious and reproducible.&#10;I can break the host configuration and recover to a working state in seconds, even if the machine is running essential services.&lt;/p&gt;&#10;&lt;h2 id="the-agentic-multiplier"&gt;The Agentic Multiplier &lt;a class="anchor" href="#the-agentic-multiplier" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I have written before about &lt;a href="https://www.nijho.lt/post/agentic-coding/"&gt;my shift towards agentic coding&lt;/a&gt;.&#10;In a world where AI agents execute tasks, &lt;strong&gt;CLI-first&lt;/strong&gt; and &lt;strong&gt;declarative&lt;/strong&gt; systems are king.&#10;An AI agent cannot reliably &amp;ldquo;click buttons&amp;rdquo; in a web interface to configure a VLAN tag or resize a disk.&#10;It needs text.&#10;It needs determinism.&lt;/p&gt;&#10;&lt;p&gt;By moving to NixOS, my entire infrastructure is defined in text files.&#10;This means my AI agents can read, understand, and even safely modify my infrastructure.&#10;Proxmox&amp;rsquo;s opaque database and UI-driven workflow were a black box to my agents.&#10;NixOS is an open book.&#10;If I want my agent to &amp;ldquo;deploy a Faster Whisper API server on port 9000 and expose it to the LAN&amp;rdquo;, it doesn&amp;rsquo;t need to navigate a &amp;ldquo;Services&amp;rdquo; menu, then a &amp;ldquo;Network&amp;rdquo; menu, and then a &amp;ldquo;Firewall&amp;rdquo; menu.&#10;It just &lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/hosts/pc/ai.nix#L385" target="_blank" rel="noopener"&gt;writes a systemd service definition&lt;/a&gt; and adds &lt;code&gt;networking.firewall.allowedTCPPorts = [ 9000 ];&lt;/code&gt; in the same file.&#10;The agent can even verify the change was successful by checking the git diff or the active configuration.&#10;This is the infrastructure counterpart to the &amp;ldquo;agentic coding&amp;rdquo; revolution I&amp;rsquo;m living in.&lt;/p&gt;&#10;&lt;p&gt;A friend who has used NixOS for many years recently complimented me on how nicely my configuration was structured, especially for managing 8 different machines.&#10;The funny thing is, I haven&amp;rsquo;t written almost a single line of my configuration myself.&#10;I did all of it using agentic AI over many sessions.&#10;I have restructured and refactored it quite a few times, going from a single machine to 2 machines, and eventually to 9.&#10;The AI handles the heavy lifting of refactoring, ensuring that my &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nixos/flake.nix" target="_blank" rel="noopener"&gt;PC, NUC, and HP&lt;/a&gt; all share common modules while keeping their unique personalities.&lt;/p&gt;&#10;&lt;h2 id="the-architecture-incus--simulation"&gt;The Architecture: Incus &amp;amp; Simulation &lt;a class="anchor" href="#the-architecture-incus--simulation" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I still wish there was a purely &amp;ldquo;Nix&amp;rdquo; way to manage persistent, stateful LXC containers and VMs.&#10;There are projects like &lt;code&gt;nixos-containers&lt;/code&gt; or &lt;code&gt;microvm.nix&lt;/code&gt;, but they often lack the operational maturity or live-migration features of a robust hypervisor.&#10;&lt;a href="https://linuxcontainers.org/incus/" target="_blank" rel="noopener"&gt;Incus&lt;/a&gt; (the community fork of LXD) fills this gap perfectly.&#10;It gives me the &amp;ldquo;cattle&amp;rdquo; management of NixOS for the host, while allowing me to run &amp;ldquo;pet&amp;rdquo; legacy workloads (like my old Ubuntu containers or Home Assistant VM) in a stable, manageable environment.&#10;Crucially, Incus is entirely controllable via a clean CLI, making it a perfect citizen in my agentic workflow.&lt;/p&gt;&#10;&lt;p&gt;Over time, I had already migrated most of my legacy LXC containers—services like DNS or media managers—to declarative Docker Compose files running inside VMs.&#10;But I still had one major holdout: &lt;strong&gt;Home Assistant OS&lt;/strong&gt;.&#10;I didn&amp;rsquo;t have a good alternative for it.&#10;I assumed I needed a dedicated appliance OS like Proxmox to run it reliably.&#10;I didn&amp;rsquo;t realize that with Incus on NixOS, running a full VM for Home Assistant is trivial.&#10;It’s just another QEMU process, but managed with the same ease as a container.&lt;/p&gt;&#10;&lt;p&gt;One neat thing I did was create Incus VMs that replicate the exact same configuration as my physical machines.&#10;Before I actually switched off my last Proxmox host, I was already confident that the full configuration worked because I had a virtual machine running the same setup.&#10;I just have a small file with &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nixos/hosts/hp/incus-overrides.nix" target="_blank" rel="noopener"&gt;overrides&lt;/a&gt;.&#10;Then I could validate that the virtual machine worked.&#10;This removed the fear of &amp;ldquo;nuking and paving&amp;rdquo; my physical servers.&lt;/p&gt;&#10;&lt;h2 id="the-migration"&gt;The Migration &lt;a class="anchor" href="#the-migration" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Migrating was surprisingly straightforward, thanks to &lt;code&gt;vzdump&lt;/code&gt; and &lt;code&gt;qemu-img&lt;/code&gt;.&#10;Here is how I moved my 7+ years of digital history without losing data.&lt;/p&gt;&#10;&lt;p&gt;I also kept some &lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/archive/PROXMOX_MIGRATION.md" target="_blank" rel="noopener"&gt;detailed migration notes&lt;/a&gt; in my dotfiles.&#10;These notes should be sufficient for anyone trying to do the same.&#10;And if not, giving them to an AI (I used Gemini 3 Pro) will certainly allow you to figure it out.&lt;/p&gt;&#10;&lt;h3 id="1-migrating-lxc-containers"&gt;1. Migrating LXC Containers &lt;a class="anchor" href="#1-migrating-lxc-containers" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;For my LXC containers (running Docker, DNS, etc.), I essentially &amp;ldquo;teleported&amp;rdquo; them.&#10;I dumped the running container on Proxmox to a standardized tarball, copied it over, and streamed it directly into a fresh Incus container.&lt;/p&gt;&#10;&lt;p&gt;On Proxmox:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Dump container 101 to a compressed archive&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;vzdump &lt;span class="m"&gt;101&lt;/span&gt; --dumpdir /var/lib/vz/dump --mode &lt;span class="nb"&gt;suspend&lt;/span&gt; --compress zstd&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On NixOS (using my &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nixos/archive/migrate-lxc.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;migrate-lxc.sh&lt;/code&gt;&lt;/a&gt; script):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Stream the backup into a new Incus container&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./migrate-lxc.sh vzdump-lxc-101-*.tar.zst ubuntu-container&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This script creates a fresh container, mounts its root filesystem, and overwrites it with the Proxmox dump.&#10;It handles the messy parts—like mapping UIDs and fixing &lt;code&gt;machine-id&lt;/code&gt;—automatically.&lt;/p&gt;&#10;&lt;h3 id="2-migrating-virtual-machines"&gt;2. Migrating Virtual Machines &lt;a class="anchor" href="#2-migrating-virtual-machines" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;For VMs (like Home Assistant OS), it was a matter of converting the disk format.&#10;Proxmox uses LVM-thin or ZFS zvols; Incus is happy with QCOW2 or raw ZFS.&lt;/p&gt;&#10;&lt;p&gt;On Proxmox:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Export the ZFS volume to a QCOW2 file&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;qemu-img convert -p -O qcow2 /dev/zvol/rpool/data/vm-100-disk-0 vm-100.qcow2&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On NixOS (using &lt;a href="https://github.com/basnijholt/dotfiles/blob/4f534bf32fb4396dd86ce631dec00717eab7656d/configs/nixos/archive/migrate-vm.sh" target="_blank" rel="noopener"&gt;&lt;code&gt;migrate-vm.sh&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Import the disk and create the VM&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;./migrate-vm.sh vm-100.qcow2 home-assistant&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="the-result"&gt;The Result &lt;a class="anchor" href="#the-result" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I now manage my entire fleet—Host OS, networking, storage, and hypervisor configuration—from a single git repository.&#10;I get the best of both worlds: persistent, stateful services where I need them, and a reproducible, bulletproof host operating system.&#10;I can wipe my host machine, reinstall NixOS, run a restore script, and be back online in minutes.&#10;Best of all, I never have to remember which checkbox I clicked in a web UI three years ago.&#10;It is all in the code.&#10;And because it is code, my agents can help me manage it.&lt;/p&gt;&#10;&lt;h2 id="references--configuration"&gt;References &amp;amp; Configuration &lt;a class="anchor" href="#references--configuration" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;All configuration files mentioned in this post are available in my &lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;dotfiles repository&lt;/a&gt;.&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/flake.nix" target="_blank" rel="noopener"&gt;&lt;strong&gt;flake.nix&lt;/strong&gt;&lt;/a&gt;: The entry point defining my entire fleet (PC, NUC, HP).&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/hosts/hp/networking.nix#L16-L31" target="_blank" rel="noopener"&gt;&lt;strong&gt;networking.nix&lt;/strong&gt;&lt;/a&gt;: The declarative fix for the Intel I219-LM network hang.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/hosts/nuc/kodi.nix" target="_blank" rel="noopener"&gt;&lt;strong&gt;kodi.nix&lt;/strong&gt;&lt;/a&gt;: NUC HTPC configuration running Kodi directly on the host.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/hosts/pc/ai.nix#L385" target="_blank" rel="noopener"&gt;&lt;strong&gt;ai.nix&lt;/strong&gt;&lt;/a&gt;: Declarative service definition for Faster Whisper.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/hosts/hp/incus-overrides.nix" target="_blank" rel="noopener"&gt;&lt;strong&gt;incus-overrides.nix&lt;/strong&gt;&lt;/a&gt;: Overrides used to simulate physical machines inside Incus VMs.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/archive/migrate-lxc.sh" target="_blank" rel="noopener"&gt;&lt;strong&gt;migrate-lxc.sh&lt;/strong&gt;&lt;/a&gt;: Script to import Proxmox &lt;code&gt;vzdump&lt;/code&gt; archives into Incus containers.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/archive/migrate-vm.sh" target="_blank" rel="noopener"&gt;&lt;strong&gt;migrate-vm.sh&lt;/strong&gt;&lt;/a&gt;: Script to import QCOW2 disk images into Incus VMs.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/basnijholt/dotfiles/blob/fa890fa22ee608be2d11ab4f705550337092c414/configs/nixos/archive/PROXMOX_MIGRATION.md" target="_blank" rel="noopener"&gt;&lt;strong&gt;PROXMOX_MIGRATION.md&lt;/strong&gt;&lt;/a&gt;: Detailed raw notes and inventory of the migration.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item></channel></rss>