I’ve been playing with Nix and NixOS a lot more lately. I installed NixOS on one of my servers, I installed the Nix CLI on my laptop, I tried to use Nix to [build a Docker (BROKEN) image], I use Nix flakes.
This post was written from the perspective of a person new to Nix, but experienced with other computer languages. Thus, it’s probable that I might be doing something wrong or maybe complaining about something that’s obvious to you. However, these are issues that others may face.
It was also written over several months as I gathered issues, so even looking back, I see mistakes.
The Good Parts
The biggest advantage about Nix is that I can define an entire system using a text based language that I can check into a Git repo. The workflow of identifying a bug, making a change, running nixos-rebuild switch, verifying it works, then checking it in. Then six months from now being able to Git blame and see why I made a change is great.

The Bad Parts
Nix, the language
The Nix language is not-intuitive. I understand it’s not like most computing languages that are designed for performing operations and it’s intended to be a declarative configuration model, but as a developer with skills in other languages, I can’t
Variables
To declare a variable, you have to do this:
| |
My problem is I might start with some code like:
| |
But if I want to refer to nginx in nginxConfig, I have to radically change this code:
| |
This requires a big change in the code to do something that is trivial in every other programming language.
Scoping
Nix, the CLI
Errors
Having useful error messages is critical for developers, but nix does not give good error messages. Now, a lot of this is because I’m new to Nix and make lots of dumb mistakes when coding, but a language needs to be approachable by new people. Otherwise, they can’t turn into experienced devs.
Let’s see an example:
| |
A file that I wrote was only mentioned at the very last point. Let’s zoom in:
| |
Can you spot the mistake? I spent time hunting through the buildLayeredImage block, but that was entirely irrelevant. The error message seems to suggest line 1 and col 1, but it’s only a {.
Turns out, the fix is (note the curly braces).
| |
It was not intuitive for me to figure that out.
Confusing Commands
There’s quite a few Nix commands:
- nix
- nix-build
- nix-channel
- nix-collect-garbage
- nix-copy-closure
- nix-daemon
- nix-env
- nix-hash
- nix-info
- nix-instantiate
- nix-prefetch-url
- nix-shell
- nix-store
- nixos-build-vms
- nixos-container
- nixos-enter
- nixos-firewall-tool
- nixos-generate-config
- nixos-help
- nixos-install
- nixos-option
- nixos-rebuild
- nixos-version
Now you can probably guess what most of these do, but that’s not what I mean. I want to upgrade my Nix daemon. How do I do it? nix upgrade? Nope.
Nix, the package manager
What I would have expected for versioning
As I understand it, when you pick a package from nixpkgs (e.g. pkgs.nginx), you’re picking what ever happens to be defined as the version in the HEAD commit in NixOS/nixpkgs repo. Using a Nix Flake can freeze the commit you’re looking at, but that is all of nixpkgs. You’re still at the mercy of whatever the nixpkg maintainer used as a version. That version could be out of date or it could even be an unofficial release candidate (e.g. this commit adopted an rc version). Or maybe the docker uses 27.x which is not officially supported by your Kubernetes engine RKE1.
What are my options? With some packages like Docker, Nixpkgs continues to maintain the old versions, so I can just change from pkgs.docker to pkgs.docker_26, but not every package has this luxury. Nor is it clear how I actually use it when I use:
| |
In the onedrive example, the change was unexpected. It appears that it’s only a release candidate in the unstable Nix channel, but unstable seems to be the default.
I could always just copy the derivation into my own repository and never deal with any issues, but that’s non-trivial.
I see the nixpkgs style of versioning to be fundamentally not user-friendly. I think I’d prefer something like programming language dependency modeling (think Ruby Bundler’s Gemfile or Python’s Pipfile) where I can do:
| |
Right now, the upgrade story brings risk for unexpected changes. I do a nix flake up and nixos-rebuild switch and things just poof change.
How do I even override something?
I’m trying to use the Kubernetes NixPkg, but I needed to modify it to:
- Pin the control plane and worker versions so I can bump the control plane before bumping the worker nodes
- Pin the pause image so it doesn’t get accidentally deleted breaking my cluster
- Not delete the CNI binaries automatically every time Kubelet starts
How do I pin NixPkgs?
To pin the versions, I found posts that said I could just import a specific commit of NixPkgs like this and then update the commit as I desire:
| |
It’s not exactly what I want which is to pin to a specific Kubernetes Major.Minor versions, but it’s fine. However, it still doesn’t make any sense:
| |
What I think is happening is that the nixpkgs-k8s parameter does point to the expected commit, but nixpkgs is composed of both configuration and packages. The configuration services.kubernetes.kubelet works off the latest commit.
My first assumption, until I was actually writing this post, was that I’d have to duplicate the services/cluster/kubernetes files and here where it builds the systemd service, I somehow modify the path to kubelet from ${top.package}/bin/kubelet to ${nixpkgs-k8s.services.kubernetes.package}
However, I now see a services.kubernetes.package option that I think I can override. But how? Maybe nixpkgs-k8s.services.kubernetes.package?
| |
Nope, I get:
| |
I’m not really sure what to do to fix this. Maybe I need to vendor the entire kubernetes/ folder myself, but I look at it and I will have to make a number of modifications to get it to work to change variable references.
Conclusion
Nix is a fascinating idea. The ability to declaratively define my entire OS is great for servers. I can have all my servers look exactly the same and easily create ones as I want. I like the idea of being able to Git revision control changes. It really satisfies my itch to centralize and cleanly define configuration.
However, I find working with the Nix language and Nix the package manager to be extremely frustrating. Usually there’s a learning curve that I can get over and become proficient enough at it, but Nix I struggle with. I’m not sure what my threshold for, it’s too difficult to be worth it is.
