Every Cluster API tutorial for Hetzner starts from Ubuntu, and the purpose-built alternative to that is Talos Linux — a dozen binaries, no SSH, one mTLS API. NixOS is a third option, and its pitch has nothing to do with minimalism. A NixOS node image is a full general-purpose Linux — SSH daemon, systemd, shell, package manager, all still there by default — whose entire configuration is one flake.nix that rebuilds bit-for-bit identical across every machine and rolls back atomically if a change breaks a node. Where Talos removes the attack surface for you, NixOS gives you the tools to eliminate configuration drift and hands you the hardening work yourself. Here's what wiring that into a Cluster API Provider Hetzner (CAPH) fleet actually looks like, and what it costs against a Talos-based one.
What NixOS Actually Changes at the Node-Image Layer
A stock CAPH node — Ubuntu plus cloud-init plus kubeadm — carries its configuration in three disconnected places: whatever the base AMI/snapshot baked in, whatever cloud-init scripts inject at first boot, and whatever an operator SSHes in and hand-patches afterward. None of those three sources is guaranteed to agree with what's on disk six months later.
NixOS collapses all three into one file. A flake.nix declares the kernel version, the exact kubelet/kubeadm/containerd package versions, which CNI binaries are present, every sysctl, and — via the disko tool — the disk partition layout itself. nix flake.lock pins every one of those inputs to a specific content hash, so building the same flake on two different machines, or on the same machine six months apart, produces a bit-for-bit identical image. There's no drift to audit because there's no path for the running system to diverge from the file that describes it.
The build step itself changed recently: nixos-generators, the tool most existing NixOS-on-cloud guides point to, was archived on January 30, 2026, with its image-format logic folded into nixpkgs proper. The current path is nixos-rebuild build-image --image-variant qcow2 --flake .#hetzner-node, run straight from the flake that also defines the running system — one less external tool between "configuration" and "bootable disk image."
Updates work the same way nixos-rebuild switch always has: applying a new generation is atomic, the previous generation stays on disk, and nixos-rebuild switch --rollback reverts a bad change without replacing the node. That's a materially different failure mode than a broken Ubuntu cloud-init run, which usually means re-provisioning the machine.
The Walkthrough: Wiring a NixOS Image into CAPH
This is the part most write-ups skip, and it's where the real cost shows up. Four steps, one important non-step.
1. Build the image. nixos-rebuild build-image --image-variant qcow2 --flake .#hetzner-node produces a raw/qcow2 disk image from the flake — kernel, kubelet, kubeadm, containerd, CNI plugins, and disk layout all baked in per the configuration.
2. Get it onto Hetzner Cloud. This is the awkward part: the Hetzner Cloud API has no endpoint for uploading a disk image directly. The only first-party path to a custom image is snapshotting a running server. Two ways to get a NixOS qcow2 there:
hcloud-upload-image— a community CLI that boots a throwaway server into Hetzner's Rescue System,dds the raw image onto the disk overqemu-nbd, then snapshots it via the API.nixos-anywhere— installs NixOS directly onto a running (rescue-mode) server over SSH usingdiskofor partitioning, then you snapshot that server the same way CAPH's own docs describe for any custom image.
Either way, the output is a named Hetzner snapshot — same artifact type a hand-built Ubuntu image produces.
3. Point CAPH at it. The snapshot name goes into HCloudMachineTemplate.spec.imageName, exactly the field a Packer-built Ubuntu image or a Talos image would use. Nothing else about the HCloudMachineTemplate or HCloudCluster resources changes. Control-plane and worker MachineDeployments can point at the same snapshot or different ones — a control-plane-specific flake with a smaller systemPackages set and a worker flake with GPU drivers or extra CNI plugins baked in, built from two nixosConfigurations in the same repo, is a normal split.
4. The non-step: bootstrap is untouched. This is the detail that decides how much of "CAPI integration" NixOS actually gives you. Talos ships its own pair of providers — CABPT and CACPPT — that replace kubeadm init/kubeadm join with Talos's own machine-config API. NixOS has no equivalent. There's a services.kubernetes module in nixpkgs, but it's a from-scratch, dev-oriented implementation that doesn't speak kubeadm and isn't what you want here — using it would mean abandoning the exact kubeadm-based bootstrap Cluster API is built around. So a NixOS-based CAPH fleet keeps the standard KubeadmControlPlane and CABPK bootstrap provider, unchanged:
clusterctl init \
--infrastructure hetzner \
--bootstrap kubeadm \
--control-plane kubeadmSame command as a stock Ubuntu fleet. NixOS replaces what installs the OS and binaries onto the node; it does not replace how the cluster boots. Talos replaces both.
What This Buys: Reproducibility With Teeth
The reproducibility isn't just a nicer word for "consistent config" — flakes make specific guarantees a cloud-init script can't:
- Every generation is content-addressed. Two nodes built from the same
flake.lockare identical down to the kernel build, not just "running the same package version" — the entire dependency closure, including build tools and patches, is pinned. - Rollback doesn't touch the fleet's node count.
nixos-rebuild switch --rollbackon a single misconfigured node reverts it in place. A bad Ubuntucloud-initrun, by contrast, typically means cordoning the node and letting Cluster API replace it viaMachineDeployment. - Diffing two node generations is
git diff configuration.nix. There's no equivalent of SSHing into two Ubuntu nodes and manually comparingdpkg -loutput or/etccontents to find where they've drifted.
Take a concrete case: bumping kubelet fleet-wide ahead of a Kubernetes minor upgrade. On the Ubuntu path, that's a new Packer build, a new AMI/snapshot, and hoping every existing MachineDeployment actually gets rolled onto it before someone provisions a stale node from the old one. On the NixOS path, it's a one-line version bump in flake.nix, a new flake.lock, and every subsequently-built node is that version — with the old generation still on disk on already-running nodes if a rollback is needed mid-rollout. The version isn't a fact about which AMI happened to get used; it's a fact recorded in the same file as everything else the node is.
What It Costs: Everything Talos Already Solved
This is the honest trade, and it's not close to free.
| Talos Linux | NixOS | |
|---|---|---|
| Attack surface | ~12 binaries, no SSH daemon, no shell, no package manager, read-only root — hardened by the Talos maintainers and shipped that way | Full general-purpose Linux by default: sshd, systemd, a shell, nix-env. Matching Talos's surface means you write and maintain services.openssh.enable = false, an empty environment.systemPackages, security.sudo.enable = false, and kernel lockdown flags yourself — unaudited DIY hardening, not a vetted default |
| CAPI bootstrap integration | Official CABPT/CACPPT providers; clusterctl init --bootstrap talos --control-plane talos | None. Standard kubeadm/CABPK bootstrap, unmodified — NixOS only owns the OS layer |
| Image-build tooling maturity | Stable, versioned Talos releases; image builds are a first-class, documented CAPH/Talos workflow | nixos-generators was archived January 30, 2026 mid-migration into nixpkgs' nixos-rebuild build-image; the Hetzner upload path (hcloud-upload-image) is an unofficial, community-maintained tool working around a missing Hetzner API endpoint |
| What you're buying | A narrower, already-hardened surface a platform doesn't have to build itself | Bit-for-bit reproducibility and atomic rollback, at the cost of building the hardening and the image pipeline yourself |
Talos's ~12-binary surface isn't a starting point you configure toward — it's the finished product of the Talos maintainers' own security review. A NixOS image that disables sshd and strips systemPackages down to nothing is a plausible destination, but it's a destination you own, test, and keep correct across every future flake change. Nothing in the NixOS module system stops a future commit from re-enabling a shell package by accident; Talos's image doesn't have a shell to accidentally re-add.
When Each One Actually Makes Sense
NixOS earns its keep when a team already runs Nix somewhere else — dev shells, CI, other services — and wants one declarative story from laptop to node, and is willing to own the hardening as ongoing engineering work rather than a one-time checkbox. Talos earns its keep when the goal is the smallest possible node attack surface with zero in-house security review, delivered turnkey through its own CAPI providers.
Neither is the default. Both are strictly better than an Ubuntu base whose configuration lives in an AMI, a cloud-init script, and an operator's shell history, in three different places that quietly drift apart.
Bex.co runs its own Cluster-API-provisioned fleet on Hetzner, and node-image choice — Ubuntu, Talos, or something like this — is exactly the kind of decision an operator running the open-source control plane gets to make for themselves. Check out the repo on GitHub.



