{"slug": "the-last-btrfs-machine-migrating-my-pc-to-zfs", "title": "The last btrfs machine: migrating my PC to ZFS", "summary": "A user migrated their last btrfs machine to ZFS after discovering their backups had silently failed for four months, driven by paranoia that an AI agent running in YOLO mode could destroy their system. The migration unified their fleet on NixOS with ZFS, snapshots, and replication declared in code, addressing inconsistencies in backup size reporting across tools like du, df, and restic.", "body_md": "My GPU machine was the last machine in my fleet still running btrfs.\nI [bought it for gaming](https://www.nijho.lt/post/local-ai-journey/) (which never happened), and these days its dual RTX 3090s almost exclusively run [local AI models](https://www.nijho.lt/post/llama-nixos/).\n\nbtrfs itself never failed me.\nWhen 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.\nThat belief was simply wrong.\nAs I [went full Nix](https://www.nijho.lt/post/proxmox-to-nixos/) and eventually [moved even my TrueNAS to NixOS](https://www.nijho.lt/post/truenas-to-nixos/), I learned there was no issue at all.\nI just had to figure out the right incantation to make ZFS work with boot drives, [disko](https://github.com/nix-community/disko), and [nixos-anywhere](https://github.com/nix-community/nixos-anywhere).\nMy 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.\n\nOnly 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.\n\nThe operating system was already uniform. The filesystem was not, and that turned out to matter more than I expected.\n\nThe real trigger was that I got increasingly paranoid about an AI agent messing up my system. I run agentic AI in YOLO mode, exclusively. All day, every day, agents execute commands on this machine without asking me first. YOLO does not mean root, though: the agents run as my regular user, and the backup credentials live where only root can read them. An agent that could destroy the backup itself would make everything else in this post pointless. Meanwhile the stories keep coming in about even the most frontier models wiping someone’s home directory:\n\nGPT-5.6-Sol just accidentally deleted almost ALL of my Mac’s files. And this is why I trust Fable 1000x more.\n\nThe reaction is always about which model deserves more trust.\nI read these tweets differently.\nGive any agent enough sessions and it will eventually delete something important, the same way I do every few years with a tired `rm -rf`\n\n.\nWhether that moment becomes a catastrophe or an inconvenience depends entirely on the restore path.\n\nSo I felt I needed to properly audit the way my backups were done. I want to be able to answer four questions about any machine I own:\n\nAsking question 1\ndelivered an unpleasant answer within the hour: the backups had quietly stopped four months earlier.\nTwo 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 *after* a successful backup, and the repository URL still pointed at `truenas.local`\n\n, a name that had stopped resolving [along the way](https://www.nijho.lt/post/truenas-to-nixos/).\nThe last successful backup ran at 07:00 on March 22; the next one at 07:16 on July 22, after [the fix](https://github.com/basnijholt/dotfiles/commit/4d2a75efcb503b9ca2aabcbe8339be3e5a62d0ce).\nFour months, almost to the minute.\nsystemd logged the failure on every single run, nobody read it, and nothing escalated.\nI found out because I asked the question, not because anything told me.\n\nOn the btrfs box, question 3\nalone had three different answers depending on which tool I asked.\n`du`\n\ncounted reflinked copies at full size, so directories reported terabytes that physically did not exist.\n`df`\n\ncounted physical extents, which is honest about disk usage but tells you nothing about what a file-based restore will write.\nAnd restic reported the *apparent* size of sparse VM images, which inflated the restore estimate to several times the disk.\n\nWhen your storage layer gives you three sizes for the same data, you do not really understand your backup. You have a backup, and you have hope.\n\nQuestion 4\nwas no better.\nBecause the PC was my only btrfs machine, it could not join [the ZFS snapshot replication the rest of the fleet uses](https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/replication.nix), so its backups relied entirely on restic pushing to the NAS.\nOff-machine, note, but not off-site; question 2\nturned out to apply to locations too, and closing that gap is its own project.\nrestic is file-based: every run visits the metadata of every single file to find what changed.\nOn 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 `node_modules`\n\n.\nEach run took about an hour and a half, even when almost nothing had changed.\n\nThat scan time dictated my backup cadence.\nThe 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 [dropped it to every six hours](https://github.com/basnijholt/dotfiles/commit/4f2401de725d5201d4daca0e67505ad2e6f6691a) to stop the madness.\nEither way, my most recent work existed only on the machine an agent was busy poking at.\n\nThe scanning is also not free for the hardware.\nReads 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.\nAnd drives stopped being cheap: I paid $227 for [a 4 TB NVMe drive](https://www.nijho.lt/post/homelab/) in 2024, and the same drive lists for around $800 today.\nI would rather not run the experiment of what years of continuous scanning do to a drive I can no longer afford to replace.\n\nbtrfs 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.\nWith ZFS I can snapshot every few minutes and `zfs send`\n\nthe actual block storage: an incremental send streams exactly the blocks that changed since the last snapshot, without walking a single file.\nA hundred million files cost nothing when nobody has to visit them.\n\nZFS does not magically collapse those numbers into one.\nBut it gives me the same semantics everywhere, including on the NAS pools that hold the backups themselves.\nEight of my machines already share a single ZFS disk layout, defined in [one shared disko module](https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/common/disko-zfs.nix).\nThe PC was the odd one out.\nThis migration makes it nine machines with the exact same strategy, and one backup story to audit instead of one per machine.\n\nThe Nix side was the easy part.\nThe PC now imports the same shared disko module as the other eight machines, with one addition: a `swapSize`\n\nparameter, because ZFS has no reliable swapfile story and the PC actually needs its 96 GB of swap.\n\nThe disk is [pinned by ID](https://github.com/basnijholt/dotfiles/blob/8c0bc5faded40362ac01347682aaf2089562047d/configs/nixos/hosts/pc/disko.nix), not by device name:\n\n```\ndevice = \"/dev/disk/by-id/nvme-Samsung_SSD_990_EVO_Plus_4TB_...\";\n```\n\nThis matters more than usual here. 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.\n\nThe scary property of this migration is different from the NAS one.\nFor the NAS, the whole point was that disko must *not* touch the data pools.\nFor the PC, the new ZFS pool goes on the **same disk the data currently lives on**.\nNo second disk, no local rollback, no btrfs to boot back into.\nFrom the moment disko runs, the restic repository on the NAS is the only road back.\n\nThat sentence is the reason most of this post is about verification.\n\nThe road back also has to survive the same agents that motivated all of this.\nThe 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.\nSo the NAS [snapshots the repository dataset on its own side](https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/storage.nix#L128-L143), where the PC’s credentials cannot reach: two days of hourly ZFS snapshots and two weeks of dailies that only root on the NAS can destroy.\n\nThe [NAS migration](https://www.nijho.lt/post/truenas-to-nixos/) taught me to rehearse the destructive step in a VM, so the flake grew a [ pc-vmtest target](https://github.com/basnijholt/dotfiles/blob/8c0bc5faded40362ac01347682aaf2089562047d/configs/nixos/flake.nix#L110) and I ran the whole install through\n\n`nixos-anywhere --vm-test`\n\n.\nDisko formatted a virtual disk, created the pool, installed the system, and booted it with swap active.Passing that test proves the generated logic. It does not prove the backup, which is where the real risk lives.\n\nA restore that starts with “first, log in to the dead machine and fetch the credentials” is not a restore plan. 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’s SSH host keys. 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. A munge key and a README round it out.\n\nMy recovery story has two layers.\nNixOS makes the *system* disposable: every service, driver fix, and firewall rule is in git, so no agent can leave the machine in a state I cannot rebuild.\nVerified backups make the *data* recoverable.\nThis migration is the voluntary version of the tweets above: I am about to delete all of my PC’s files on purpose.\n\nI did three verifications, each one converting an assumption into a fact. An AI agent did the tedious sweeps; I reviewed the evidence.\n\nThe first check was completeness.\nWe walked everything on the machine *outside* the backup paths, until every byte was accounted for.\nIt was all re-downloadable caches, and the largest unexplained thing turned out to be restic’s own local cache.\nEverything outside the backup set is now *known* disposable instead of *assumed* disposable.\n\nThe second check was the restore path. A backup you have never restored from is a hypothesis. 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. The hashes matched byte for byte, and even the symlinks came back pointing at the right targets. A sample proves the path works, from credentials to symlink handling; reading back every byte is the third check’s job. The machine running the rehearsal had the kit and nothing else, so recovery provably does not depend on the machine being recovered.\n\nThe third check is the data itself.\n`restic check`\n\nvalidates the repository structure without reading back the actual data blobs.\nBefore the wipe, a `--read-data`\n\npass ran on the NAS, where the repository sits on local disk instead of behind the network.\nWhen the backup is the only rollback, I want its bits read back at least once.\n\nThe outage taught me a fifth question\n: how do I find out when any of this stops working?\nAll three checks above are point-in-time, and verification rots.\nSo the NAS now [re-verifies every hour](https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/replication.nix#L263-L285), 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.\nThat catches every failure mode on the PC side, including “the timer is simply disabled.”\nThe watcher itself is watched: the NAS pings an [external dead-man’s switch](https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/health.nix#L252-L280) every five minutes, so if the machine holding my only road back goes dark, the alert comes from outside the house.\n\nWhile wiring this up we discovered that months of earlier NAS alerts had never arrived, because [ntfy silently rejects message bodies over 4 KB](https://github.com/basnijholt/dotfiles/blob/0d6101c8e3f91a0845cb41adb3cda216f50917c8/configs/nixos/hosts/nas/health.nix#L60-L67).\nA notification you have never received is also a hypothesis.\n\nThe measured restore set came out to roughly 1 TB, and 99.9% of it is `/home`\n\n.\nBut 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).\n\nI do not need either of those to have a working machine. So the restore script now runs in stages:\n\n```\nsudo ./restore_from_backup.sh stage1 # everything else → a fully working machine\nsudo ./restore_from_backup.sh stage2 # Steam + models, whenever\n```\n\nStage 1 restores everything except the deferred paths, plus the system identity (host keys). Stage 2 can run days later, from the rebooted system, while I am already using the machine.\n\nOne detail I care about: stage 1 resolves `latest`\n\nto a concrete snapshot ID and pins it to a file.\nStage 2 reads the pin.\nMy backups run every six hours, so without this, a timer firing between the stages would silently split the restore across two different snapshots.\n\nThe backup itself is untouched: Steam and the models are still in every snapshot. Only the restore order changed.\n\nThe PC had also quietly become an Incus host for three LXC containers, including the [MindRoom bots](https://www.nijho.lt/post/mindroom/) my family talks to every day.\nBefore the migration, those containers moved to the NAS.\nThat shrinks both the backup and the restore, and the family bots keep running while their old host is being wiped.\n\nThe install followed the same phased `nixos-anywhere`\n\npattern 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.\nThat checkpoint is the whole point: right up to the destructive phase, aborting was still cheap.\nThe destructive part turned out to be the trivial part: from wiping the disk to booting the new system took less than twenty minutes.\n\nThe one surprise of the day was in the restore, and it was exactly the kind rehearsal is supposed to catch. 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. The key it needed sat in the recovery kit inside my home directory. The rehearsal never hit this, because the rehearsal machine’s root had already talked to the NAS. A verified restore path can still depend on state you forgot you had.\n\nStage 1 restored 534 GiB across 4.5 million files in just over an hour and a half, at wire speed.\nThe machine rebooted with its old host keys and its old `/home`\n\n, and stage 2 started immediately.\nThe 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.\nThe deferred 648 GiB of games and models took six more hours in the background.\n\nWith that, the PC finally joins the fleet pattern it was locked out of: sanoid snapshots the pool every hour, so a tired `rm -rf`\n\n, mine or an agent’s, now costs at most an hour of work and a rollback, and a daily syncoid run sends the changed blocks to the NAS.\nrestic is on borrowed time.\nIts 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.\n\nAftercare 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.\n\nThe slow work, as always, was not the cutover. The slow work was making the cutover boring.", "url": "https://wpnews.pro/news/the-last-btrfs-machine-migrating-my-pc-to-zfs", "canonical_source": "https://www.nijho.lt/post/btrfs-to-zfs/", "published_at": "2026-07-23 00:00:00+00:00", "updated_at": "2026-07-24 06:34:30.137103+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "artificial-intelligence"], "entities": ["NixOS", "ZFS", "btrfs", "TrueNAS", "disko", "nixos-anywhere", "restic", "GPT-5.6-Sol"], "alternates": {"html": "https://wpnews.pro/news/the-last-btrfs-machine-migrating-my-pc-to-zfs", "markdown": "https://wpnews.pro/news/the-last-btrfs-machine-migrating-my-pc-to-zfs.md", "text": "https://wpnews.pro/news/the-last-btrfs-machine-migrating-my-pc-to-zfs.txt", "jsonld": "https://wpnews.pro/news/the-last-btrfs-machine-migrating-my-pc-to-zfs.jsonld"}}