Cached at:
06/08/26, 05:20 PM
# nixidy part 1: Introduction to nixidy | codedbearder
Source: [https://codedbearder.com/posts/nixidy-part-1-introduction/](https://codedbearder.com/posts/nixidy-part-1-introduction/)
## nixidy part 1: Introduction to nixidy
Posted on Jun 8, 2026
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](https://github.com/arnarg/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\.
By the end of this part we'll have a working nixidy project that defines an nginx Deployment and Service, generates Argo CD`Application`manifests automatically, and deploys to your cluster through GitOps\.
## What you'll build
We're going to build a nixidy environment called`dev`containing one application deployed to your cluster via Argo CD\. The project structure will be the skeleton you'd extend to manage an entire production cluster\.
## Prerequisites
- **Nix**installed with flakes enabled \([download](https://nixos.org/download.html)\)
- **A Kubernetes cluster**with**Argo CD**installed
- **A Git repository**for your Kubernetes manifests \(GitHub, GitLab, etc\.\)
- **Basic familiarity**with Kubernetes Deployments, Services, and Namespaces
- **Basic familiarity**with Argo CD`Application`resources
info
nixidy implements the[Rendered Manifests Pattern](https://akuity.io/blog/the-rendered-manifests-pattern/)where your CI generates plain YAML, you review it in PRs, and Argo CD deploys it\. If you've used Argo CD with raw YAML or Kustomize before, the deployment side is identical\. The difference is entirely in*how the YAML is produced*\.
## A Nix expression that builds a Kubernetes manifest
The core idea behind nixidy is that every Kubernetes resource is a typed Nix option\. A Deployment isn't a blob of YAML, it's a structured attribute set where`replicas`is an integer,`image`is a string, and a typo in`selector`is a build error, not a runtime surprise\.
Let's start by creating the project:
```
mkdir my-cluster && cd my-cluster
git init
```
Now create`flake\.nix`, this is the entry point that wires nixidy into your Nix flake:
```
{
description = "My Kubernetes cluster managed with nixidy";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
nixidy.url = "github:arnarg/nixidy";
};
outputs = {
nixpkgs,
flake-utils,
nixidy,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
in {
nixidyEnvs = nixidy.lib.mkEnvs {
inherit pkgs;
envs.dev.modules = [ ./env/dev.nix ];
};
});
}
```
A couple things worth noting:
- `nixidy\.lib\.mkEnvs`takes a set of named environments and returns Nix derivations that build YAML manifests\. The key`dev`becomes the attribute you reference with`\.\#dev`\.
- Each environment takes a list of NixOS\-style modules which are plain`\.nix`files that set options\. This is the same module system that powers NixOS, which means you get`imports`,`lib\.mkDefault`,`lib\.mkForce`, and all the composition primitives you'd expect\.
info
If you've configured a NixOS system before, the shape is identical: a list of modules that set options, merged by the module system\. The difference is that the options describe Kubernetes resources instead of system services\.
## An environment module with one application
Now let's create the environment directory and the dev module:
Write`env/dev\.nix`\. Make sure to replace the repository URL with your own \(this is where nixidy will tell Argo CD to look for rendered manifests\):
```
{
nixidy.target.repository = "https://github.com/YOUR_USERNAME/my-cluster.git";
nixidy.target.branch = "main";
nixidy.target.rootPath = "./manifests/dev";
applications.nginx = {
namespace = "nginx";
createNamespace = true;
resources = {
deployments.nginx.spec = {
replicas = 2;
selector.matchLabels.app = "nginx";
template = {
metadata.labels.app = "nginx";
spec.containers.nginx = {
image = "nginx:1.25.1";
ports.http.containerPort = 80;
};
};
};
services.nginx.spec = {
selector.app = "nginx";
ports.http.port = 80;
};
};
};
}
```
Let me walk through what this declares:
- **`nixidy\.target\.\*`**: Where the generated YAML ends up in your Git repo\. Argo CD`Application`manifests will reference this repo, branch, and path\.
- **`applications\.nginx`**: One logical application\. An application gets its own directory in the output and its own Argo CD`Application`manifest\.
- **`namespace = "nginx"`**: All resources in this application are deployed to the`nginx`namespace\.
- **`createNamespace = true`**: Nixidy generates a`Namespace`manifest automatically\. Without this, you'd need to create the namespace out\-of\-band\.
- **`resources\.deployments\.nginx`**: A typed Deployment\. The`spec`attribute follows the[Kubernetes Deployment spec](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/deployment-v1/), but enforced at Nix evaluation time\.
- **`resources\.services\.nginx`**: A typed Service, same idea\.
Why not just write the YAML?
Two reasons\.
1. Type errors become build errors\. Set`replicas = "two"`in the module above and`nixidy build`fails immediately, not 15 minutes into a deployment rollout\.
2. Composition\. When you add a`prod\.nix`that imports this same module and sets`replicas = lib\.mkForce 10`, you're expressing "same app, different scale" in two lines instead of copying an entire YAML file and changing one number\. The NixOS module system \(`imports`,`lib\.mkDefault`,`lib\.mkForce`\) gives you this for free, and it's the same mechanism that handles multi\-environment NixOS configs\.
## Build the manifests
Run the build:
```
nix run github:arnarg/nixidy -- build .#dev
```
info
The first run downloads nixidy and its dependencies into the Nix store\. Subsequent runs are instant if nothing changed\.
Inspect the output:
You should see:
```
result/
├── apps/
│ └── Application-nginx.yaml
└── nginx/
├── Deployment-nginx.yaml
├── Namespace-nginx.yaml
└── Service-nginx.yaml
```
Look at the generated Deployment:
```
cat result/nginx/Deployment-nginx.yaml
```
```
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx:1.25.1
name: nginx
ports:
- containerPort: 80
name: http
```
And the Argo CD`Application`that nixidy generated for you:
```
cat result/apps/Application-nginx.yaml
```
```
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: nginx
namespace: argocd
spec:
destination:
namespace: nginx
server: https://kubernetes.default.svc
project: default
source:
path: ./manifests/dev/nginx
repoURL: https://github.com/YOUR_USERNAME/my-cluster.git
targetRevision: main
```
Every`applications\.\*`block produces exactly one Argo CD`Application`pointing at the directory containing its rendered manifests\. This is the rendered manifests pattern: Argo CD syncs plain YAML, not templates, not Helm releases, just static files it can diff against the cluster state\.
## Commit the rendered manifests
The`nixidy switch`command copies the built manifests into your repository at the`rootPath`you configured:
```
nix run github:arnarg/nixidy -- switch .#dev
```
This creates`\./manifests/dev/`with the same directory tree as`result/`\. Commit and push:
```
git add .
git commit -m "Add nginx application via nixidy"
git push
```
The rendered YAML is now in your repository\. Argo CD can see it\.
## Deploy to your cluster
### Bootstrap with Argo CD
If Argo CD is already running in your cluster, one command creates an "app of apps" \(a parent`Application`that manages all your nixidy applications\):
```
nix run github:arnarg/nixidy -- bootstrap .#dev | kubectl apply -f -
```
This outputs an Argo CD`Application`manifest that points at`manifests/dev/apps/`in your repo\. Argo CD reads that directory, discovers`Application\-nginx\.yaml`, creates the nginx`Application`, which then syncs the Deployment, Service, and Namespace into your cluster\.
### Or: apply directly \(for testing\)
If you want to skip Argo CD temporarily, a local`kind`cluster for instance:
```
nix run github:arnarg/nixidy -- apply .#dev
```
This runs`kubectl apply \-\-prune`with the correct label selectors, so resources removed from your nixidy config are also removed from the cluster on the next apply \(if resources have been removed\)\.
## What's next
We now have one application in one environment\. Real clusters have a dozen applications across dev, staging, and production and I don't want to copy\-paste the same Deployment into three files\. In[Part 2](https://codedbearder.com/posts/nixidy-part-2-multi-env-and-helm-charts/)we'll refactor the nginx application into a shared module, override`replicas`per environment with`lib\.mkDefault`and`lib\.mkForce`, and integrate a Helm chart without giving up type safety\.
\* \* \*
[](https://github.com/arnarg)[](https://www.linkedin.com/in/arnari)[](https://floss.social/@arnar)
© Arnar Gauti Ingason
This website is generated with[nixtml](https://github.com/arnarg/nixtml)