Every change we've made so far has been local: edit a Nix file, run nixidy build, inspect result/, run nixidy switch
to copy manifests into the repo, commit, push. That workflow works for one person. It breaks the moment a second person needs to review what's being deployed, because they see a Nix diff, not a YAML diff, and they have to trust that nixidy build
produces what they expect.
The rendered manifests pattern exists to solve this. CI builds the YAML from the Nix files and commits it to a known location in the repository (a promotion branch, a subdirectory on main, or a separate repository entirely). The reviewer sees a plain git diff
of Kubernetes YAML. Argo CD picks up the change and deploys it. The Nix files are the source of truth; the YAML is the review artifact.
Type checking catches the wrong shape of data: a string where an integer belongs, a missing required field. But it can't catch semantic errors like a production Deployment with zero replicas, a namespace that isn't being created, or two applications that must always be deployed together. These are invariants about the cluster configuration that no schema can express.
nixidy borrows the NixOS assertion and warning system for exactly this. Assertions are build-time hard constraints: if one fails, the build stops. Warnings are advisory, they print to stderr but don't block the build. Both are evaluated during nixidy build, before any YAML reaches the repository or the cluster.
Every Kubernetes resource we've defined so far (Deployments, Services, Ingresses, Namespaces, ConfigMaps) has been typed. We didn't install anything extra to make that happen, nixidy ships with typed options for all core Kubernetes resources, generated from the official JSON schemas. When I write replicas = 3, the module system checks that 3
is an integer. When I write replicas = "three", the build fails with a type error naming the exact option and the type it expected.
That type checking extends beyond built-in resources too. nixidy includes a code generator that produces typed Nix options from any Custom Resource Definition, so my Cilium network policies, cert-manager certificates, and Prometheus service monitors get the same build-time validation as a plain Deployment. This part covers how the built-in types work under the hood, how to generate types from CRDs, and how to handle resources that don't fit the typed model.
The third time I write a Deployment + Service + Ingress trio I start to notice the shape: selector labels that match the pod template labels, a service port that mirrors the container port, an ingress that references the service by name. Every field is wired to every other field, and a typo in one label breaks the chain silently. By the fifth web application I'm copying an existing module and changing five values and hoping I changed all five.
nixidy's template system captures that pattern once, with typed options for the variables and an output
function that generates the resources. I can instantiate it with different image, port, and replicas
values and get a complete application each time. No duplication, no missed wiring.
In
the previous part
we created a single nixidy application. The nginx
Deployment
in
dev.nix
is 20 lines. When I add
staging.nix
and
prod.nix
I'll have three copies of those 20 lines, and they'll be identical except for
replicas
and maybe an annotation or two. Change the container port in one, forget it in another, and I've got a silent divergence that no CI check will catch.
The NixOS module system solves this the same way it solves duplicate NixOS host configs: shared base modules,
imports, and priority primitives that let me express "same app, different scale" in two lines instead of a full file copy. This part covers that composition, then adds a Helm chart to the mix (because most real clusters run at least one piece of software that only ships as a Helm chart).
I have managed many GitOps repositories for Kubernetes with ArgoCD and I'm sure I'm not alone in having opened a Helm values override file that was 600 lines of YAML and still not being sure which values actually made it into the rendered manifests. I've run
helm template, piped it through
grep, given up, committed it anyway and hoped the staging diff would catch anything my eyes missed.
That gap between what you
think
you're deploying and what actually lands in the cluster is exactly what
nixidy
is meant to close. I wrote it to replace Helm value files, Kustomize overlays, and raw YAML with a single Nix expression per environment. Every field is typed, every build is reproducible, and the output is plain YAML you can
git diff
before it ever touches a cluster.
In a
previous post
I wrote about how I setup NixOS on my Terramaster F2-221 instead of using the included TOS provided by Terramaster. This in itself was quite simple as the NAS contains Intel J3355, a standard X86_64 CPU. However the NAS only has 2 SATA connectors, both of which were being used for the 4TB hard drives, so I had to resort to plugging in an external USB SSD for storing the operating system. This quickly became a little annoying to make room for this external SSD behind the NAS and make sure it's always plugged in when something is moved around in the shelf where I keep it, so I wanted to see if I could come up with a better solution.
There's one thing I really don't like about many popular ARM SBCs (Single Board Computers) that for some reason has been deemed acceptable and that is the lack of on-board flash for storing the bootloader. This means that the bootloader (most often u-boot) needs to be written to a specific location on the SD card or eMMC (if available). Generally distributions for such boards offer an image for download that can be written as is to the boot medium, including the bootloader, requiring such images to be created for each supported SBC. Wouldn't it be nice if we could just pop in a generic installer USB stick where we can partition the drive as needed before installing, like is done with generic x86 computers?