tl;dr

  • I’m refactoring my system flake using The Dendritic Pattern
  • I’m testing the system flake using nixos-shell
  • I’ll completely wipe my laptop’s NixOS installation when ready

Table of contents

Motivation (some ranting)

Last week I experimented with nixos-anywhere, deploy-rs and disko on Hetzner which lets me easily deploy NixOS on a Hetzner VM even though NixOS is not one of the dashboard options.

I had some problems deploying nixos-anywhere from a MacBook (aarch64-darwin) to an x86_64-linux VM. Problems with cross-compilation, package signing, and so on. One approach to this is to have a Nix binary cache (Attic) of the same architecture, another is to run a patched version of nixos-anywhere with your local signing keys added.

All of this trouble, however, made me question if things were easier using NixOS as the host system: I have been using a MacBook for private matters because its battery is suitable for very sporadic usage, as well as couch durability with a small kid in the house. But my old Lenovo T14 isn’t bad on battery, and I’ve wanted to run a 12 CPU-core Kubernetes cluster for homelabbing anyways.

What is a dendritic flake?

In a conventional NixOS flake, flake.nix tends to grow into a single large file that defines every output: NixOS configurations, packages, devShells, formatter settings, and so on. As the system grows, so does the file. See for example terranix’es flake.nix for something that is doing way too many things at once.

You can easily prevent flake.nix bloat by placing separate things in separate files and import‘ing them. That’s what I do in hcloud-upload-image’s flake.nix: the package lives in default.nix, the dev shell lives in shell.nix; here, flake.nix only contains the list of things it exports, but not the meat.

In a dendritic flake, the flake.nix is a thin root that delegates everything to a directory of modules:

{
  description = "mechanicus.xyz distributed, dendritic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";

    flake-parts.url = "github:hercules-ci/flake-parts";
    flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";

    import-tree.url = "github:vic/import-tree";
  };

  outputs = { self, flake-parts, import-tree, ... }@inputs:
    flake-parts.lib.mkFlake
      { inherit inputs; }
      (import-tree ./modules);
}

import-tree recursively discovers every .nix file under modules/ and produces a single flake-parts module with imports = [ ... ]. Each file is a self-contained flake-parts module that can define NixOS configurations, packages, devShells, or anything else flake-parts supports. Adding a new concern means adding a new file – flake.nix itself never changes.

Note: The Dendritic Pattern is not restricted to flakes or flake-parts. The import-tree docs give examples of applying the pattern [without flake-parts][den-1] and [without even flakes][den-2] by using with-inputs and npins for flake-like inputs that are pinned.

With flakes, flake-parts and import-tree, every module is a flake-parts module.

What is nixos-shell?

nixos-shell is not nix-shell.

They solve completely different problems:

  • nix-shell drops you into a shell environment with specific packages available. It does not boot an operating system. It is analogous to nix develop in the flake world: It just changes the environment variables on your host system.

  • nixos-shell boots a full NixOS system inside a QEMU virtual machine and drops you into its console. It evaluates a nixosConfigurations flake output (or a standalone NixOS module), builds a VM image, runs it and drops you into a shell with the current working directory mounted. The VM has its own kernel, init system, systemd services, users – everything a real NixOS install has, but ephemeral and local.

nixos-shell is useful for testing NixOS configurations before deploying them to real hardware or a cloud instance. It provides a tight edit-build-boot feedback loop: change a module, run nixos-shell, and see the result in seconds.

Under the hood, nixos-shell 2.x works by calling extendModules on your nixosConfigurations output to inject its own modules. These modules set up QEMU with a serial console, mount your $HOME into the VM via 9p, auto-login as root, forward your $TERM and $PATH, and disable the firewall.

The bootstrap