I’m really liking NixOS. I originally installed it as my daily driver on my personal machine in order to have reproducible, declarative operating system builds on top of an immutable operating system so that, in the spirit of infrastructure as code (IaC), I can look into a file (or files) and see every single configuration detail of my operating system. Operating systems accumulate cruft over time, and I’ve seen running “mystery machines” that have been up for a long time, hosting important applications, often changing ownership, with numerous people configuring them over time. No one fully understands the system, but it’s working, so just don’t touch it…!!
NixOS, with its IaC and immutable nature, solves the mystery machine problem. With all important OS files read-only to users and configuration ONLY allowed through the config files, it forces us flawed humans to be responsible… (even if we only have to do just this ONE little thing real quick). Ansible and other config management tools are great, but they are still vulnerable to drift if people still have root access to the host. Human nature being what it is, this is just laying the groundwork for a future mystery machine.
I’m liking this declarative way of defining my OS so much that I’ve put it on all my systems, including my work dev laptop, and this has been working flawlessly for almost a year for me, with the caveat that I’m not doing anything too wild on my Linux boxes. It’s been able to function well for all my IDEs, AI CLIs (Gemini/Claude), browsers, files, Java SDK, any and all programming languages, and more. Every package that I need has been available, and there’s a massive package repo of 100K+ packages for Nix, one of the largest for Linux distros, if I’m not mistaken. I still use Ansible to do the initial setup of my local user, as I wanted that in a cross-OS compatible package.
One of the coolest parts of Nix is that it has an interesting way of organizing dependencies and packages. It puts every single package installed into the directory /nix/store, and using clever tricks with hashing, it can save infinite combinations of versions and configurations of packages in that directory and expose them on the host via clever tricks with symlinks and paths. This brings all kinds of value, such as rolling back to previous package versions/configs by simply changing symlinks, or allowing per-directory package configurations/versions by entering a shell in that directory with a declarative config file.
The nix-shell command is what allows you to define a custom shell.nix file in a directory and enter into that declarative, custom environment. The industry has been trying to do this kind of thing forever with tools like Docker, virtualenv, rbenv and other runtime programming managers, direnv, etc., etc. We try to create reproducible environments on a per-project basis that typically aren’t synchronized with the host’s global environment or other projects also on the host. All of the methods I mentioned up to this point have seemed imperfect to me for this particular use case, but nix-shell is the declarative environment solver from what I can tell so far. I define a file like so:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
ruby
bundler
bundix # Optional: Helpful if you want to convert Gemfiles to Nix later
nodejs # Required by Jekyll asset pipelines
gnumake
gcc # Replaces build-essential for compiling C extensions
pkg-config
libyaml # Common dependency for Ruby gems
];
shellHook = ''
# Configure Bundler to install gems locally within the project directory
# This avoids permission issues and keeps your Nix environment clean
export GEM_HOME="$PWD/.gems"
export BUNDLE_PATH="$PWD/.gems"
export PATH="$GEM_HOME/bin:$PATH"
echo "🎨 Nix environment loaded!"
echo "Ruby version: $(ruby -v)"
echo "Run 'bundle install' to set up your Jekyll environment."
'';
}
This is the configuration file for building the Middleman static site generator and subsequently building this blog from that. I simply enter the directory and type nix-shell, my shell.nix file is read and used, and now all of these packages are instantly available to me on the CLI (after the first install). I was using a Docker container before, but this feels so much more elegant, simpler, and cleaner for the purpose of local development environments.
All is not perfect in the world of NixOS; I have one minor criticism. NixOS uses its own custom DSL. It’s an entire programming language, and functional at that, so it’s a bit foreign and there’s a significant learning curve to become fluent. It could be interesting if there were other programming language bindings, such as Python or Go. This isn’t the end of the world, though, as the Nix language is pretty readable, so you can use an LLM to generate the file(s) and then read over them to make sure things look correct.
Another quibble is that it’s tricky figuring out what packages have baked-in functionality in the Nix config file/DSL and what needs to be configured outside of Nix. For example, Firefox can be installed via the normal package manager function in the config file like a normal package and configured via files/settings on the OS, just like on any other host… but Firefox ALSO has its own configuration bindings inside of the Nix config file/DSL, which allows you to configure numerous settings for Firefox inside the Nix config file itself. This type of thing is common across packages in Nix, as many packages have Nix specific bindings and can be configured entirely inside the Nix file, Nginx and Apache for example, without having to configure the actual web servers’ configuration files.
These are minor quibbles and are technically solved by LLMs, as they can simply give you the configuration code you need to add to the file. As a matter of fact, Nix works amazingly well alongside LLMs because you have easily modifiable file targets for the LLM, you have diffs for all of your changes, and you have an easy rollback if the LLM breaks anything. I can see this being a really powerful way to configure production servers at scale in the future. Why use Ansible or Puppet when you have a configuration management tool built INTO the operating system? (I’m being a little hyperbolic, as I’m sure there might be arguable reasons for standalone configuration management over a Nix setup.)
Ultimately, while NixOS has a steep learning curve and its own set of idiosyncrasies, the peace of mind it provides is great. For anyone tired of dealing with configuration drift and the anxiety of the “mystery machine,” NixOS feels like the logical conclusion of how modern systems should have been built all along.