nixos – the last operating system you’ll need

[or] is it?

Nov 14 2025

Tags: linux nix

#

Prologue

It was a rainy weekend, and after brewing a mugfull of coffee I sat comfortably and opened my laptop that I powered off yesterday after running an sudo pacman -Syuu yesterday to keep my Arch up to date. I like keeping things nice and up-to-date you know. The first red flags came when my fingerprint recognition wasn’t working when I tried to log in – but that’s fine, I can fix that later, not a biggie. Then the bluetooth was not seeing any devices; after 20 minutes of twiggling, reinstalling, restarting services, it did ultimately find, but didn’t cast audio through that. That’s ok, we’ve been there before, right? Wait why does my dGPU not turn on? Ok, let’s try reinstalling the drivers and cleaning some of processes, restarting. Ok now my bluetooth doesn’t work again – odd. Wait dGPU also still doesn’t work, let me remove the drivers completely… oh wait you also want to remove HIP SDK? But I need that for work… I mean, sure, ok I’ll install it again.

Morning slowly drifted to late afternoon, while I realized I have not spent a minute of that time on what I originally intended to do – recreational coding (that’s what all the normal people do on weekends, right?). After ultimately fixing all my issues I sat silently staring at my laptop, realizing I’m now roughly where I left things yesterday evening, except I burnt through a good fraction of my weekend scrolling through the Arch wiki and oftentimes toxic forums, the recurring suggestion from which was “if you can’t handle it, maybe you shouldn’t have used Arch to begin with.”

Well… maybe I shouldn’t have.

For the background – I do a lot of coding for work. I don’t consider myself a programmer (a good one anyway), yet I spend an unreasonable amount of time writing software that ultimately runs on some of the largest supercomputers in the world burning through millions of CPU- and (slightly less) GPU-hours. The reason I bring this up, is because one of the rules I learned about writing codes used by many people – is that the usability of a software is the responsibility of the developer. Just as much as writing the actual code logic itsel, making it usable, documenting, and minimizing the scope of misuse and errors is the integral part of the development process. Yes, there is only so much a developer can do, especially one working on the code during their free time. Yet I still think diverting the usability issues of a software on “you should have read through tons of literature before using it” is doing a disservice not only to the open source community, but also the software itself and, obviously, to its userbase.

Disclaimer: this is not a manual for how to set up and configure NixOS. There are plenty of these online written by people far more knowledgable than I will ever be. Instead, this is more of an emotional diary of my experience with all the features NixOS has to offer and all the places where these fail.

#

NixOS

During that rainy weekend after the unsatisfying victory over my own ADD and the unyielding systemd, I remembered about that one operating system everyone was talking about – the NixOS. The stable and reproducible. The declarative and the most reliable. The last one you will ever need. All these promises did indeed scratch just the right itches I’ve been having at the moment, so the decision was made then and there to go for it.

After reading a couple of guides on how to install NixOS, and what its configuration.nix entails, I decided to simply let it happen and figure things out on the go. I installed a bare-bones version (no flakes/home-manager) with no desktop environment, and after a short while was left with a command line interface and with only a man page describing that all I needed to do now, was to populate the /etc/nixos/configuration.nix, which, at the time, looked rather minimal.

/etc/nixos/configuration.nix
{ config, lib, pkgs, ... }:

{
  imports =
    [
      ./hardware-configuration.nix
    ];

  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;

  networking.hostName = "nixwrk";
  networking.networkmanager.enable = true;

  time.timeZone = "America/New_York";
  i18n.defaultLocale = "en_US.UTF-8";

  users.users.hayk = {
    isNormalUser = true;
    home = "/home/hayk";
    extraGroups = [ "wheel" ];
  };

  programs.firefox.enable = true;

  system.copySystemConfiguration = true;
  system.stateVersion = "24.11";
}

After some quick search over what each of the relevant lines were responsible for, I realized that I had no fucking clue what I was doing, and had no intuition whatsoever on how internally each of these toggles were doing. Like, sure, if you add magical

  environment.systemPackages = with pkgs; [
    vim
  ];

into the main curly braced scope, the system will, unsurprisingly, install vim which will be available at a rather unusual path /run/current-system/sw/bin/. Ok, I mean that’s odd, but who said /usr/bin/ is the right way to go anyway. But see… this was the precursor to what would become a complete reshaping of what I knew (or thought I knew) about Linux, how it works and how it organizes packages, libraries, applications etc. You see, what NixOS does, is it fetches and installs the package into a rather unpredictable directory, like: /nix/store/<SOME RANDOM HASH>-<PACKAGE NAME>-<PACKAGE VERSION>/bin/<EXECUTABLE>, and the symlinks it to globally accessible (analogous to /bin/ or /usr/bin/) which in this case is the /run/current-system/sw/bin/. And in fact, it takes things one step further: you see, /run/current-system/ itself is a symlink to a version of the NixOS itself: /nix/store/<SOME RANDOM HASH>-nixos-system-<HOSTNAME>-<VERSION>, meaning that each time you rebuild the system, it will essentially be stored just like any other package with symlinks connecting the dots. I mean… sure… as long as my participation in all of this is marginally unnecessary, I’m happy to roll with it (spoiler alert, it gets more complicated).

Speaking of rebuilding the system, you will be doing that quite a lot. In fact each time you install a new package or application, or each time you change a configuration in your .rc files – you will need to rebuild the system. No worries though, if the changes are small, the rebuilding procedure takes only a few seconds, and the apps are deployed right away without you having to restart the system or anything like that. Neat.

#

Flakey NixOS

It was time to step up the game. I mean, sure, having all my packages listed neatly in a list in a single .nix document was great already, but I kept wondering about this Flake thing everyone was talking about. To understand what Flakes are, I think we first need to understand what the nixpkgs is. nixpkgs is the giant repository that contains (almost) every package and NixOS module you use. Traditionally you’d point your system at a channel (e.g., nixos-24.11) and updates would move that pointer forward. That works… until it doesn’t. Your workstation moves ahead, your laptop lags behind, the server is on a different minor release, and suddenly you’re juggling three subtly different worlds.

Flakes fix that by giving you two small, explicit files in your repo:

In other words: no more “which channel am I on?”; instead: “my entire system is defined by this repo, at these precise revisions.”

If you’re struggling in understand what these inputs are, what do they actually do – I don’t either! Not fully anyway. I think of them as instructions for building a specific package (or a set of packages) tied to a specific version of nixpkgs. Meaning all the dependencies and shared libraries will be resolved using a very specific version of nixpkgs collection.

Do you absolutely have to use Flakes? I guess no (unless you wanna be one of the cool kids), at the beginning when you’re just starting with Nix, this might add some unnecessary complications to an already new and uncharted OS, which might not yield direct benefits. After all, the whole point of switching to Nix was sort of the simplicity and full control, right?

In fact, I’ve been mainly been using flakes to install packages which are otherwise unavailable within the traditional nixpkgs, for example, here are some of my inputs:

plasma-manager = {
  url = "github:nix-community/plasma-manager";
  inputs.nixpkgs.follows = "nixpkgs";
  inputs.home-manager.follows = "home-manager";
};
thorium = {
  url = "https://flakehub.com/f/Rishabh5321/thorium_flake/0.1.78";
  inputs.nixpkgs.follows = "nixpkgs";
};
zen-browser = {
  url = "github:youwen5/zen-browser-flake";
  inputs.nixpkgs.follows = "nixpkgs";
};
nogo = {
  url = "github:haykh/nogo";
  inputs.nixpkgs.follows = "nixpkgs";
};

And, yes, as you might have noted, I also managed to wrap some of my own applications in this Flakey ecosystem.

#

The Home Manager

home-manager felt like the point where Nix stops being a system config and starts being a lifestyle choice. It takes the junk drawer that is ~ – dotfiles, theme knobs, “just this one little script” you’ve been copying between machines-and gives it the same declarative, pinned treatment as the OS.

Do you need it on a single-user box? Honestly… not really. You can throw everything into configuration.nix (or other files you “import” from the flake.nix) and call it a day. For one laptop, one user, one lifetime, that’s perfectly valid. In fact, adding home-manager is another layer, another set of modules, another place to make mistakes. If you’re allergic to ceremony, keep it simple.

Where it clicked for me:

The trade-off: another moving part, more concepts (“home modules,” home.stateVersion, etc.), and the temptation to shove everything into home-manager even when a plain file would do. I still let some stuff be boring files when that’s simpler (like the lazy NeoVim configuration); not everything needs to be a module. But for the pieces I do care to reproduce, HM is the difference between “it worked on that other laptop I don’t have anymore” and “it works here because I said so.”

If you’re running a single-user system and the idea of one more layer makes your eye twitch, skip it for now. You can always add home-manager later without tearing down your world.

#

Bringing it all together

So… at the end of the day the central hub which ties everything together is the single flake.nix file which imports basically everything you need. Here’s what I ended up designing:

flake.nix
{

  description = "master flake";

  inputs = {
    nixpkgs = {
      url = "nixpkgs/nixos-25.05";
    };
    nixos-hardware = {
      url = "github:NixOS/nixos-hardware/master";
    };
    home-manager = {
      url = "github:nix-community/home-manager/release-25.05";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    # a bunch of other inputs
  };

  outputs =
    inputs@{
      nixpkgs,
      home-manager,
      ...
    }:
    let
      cfg = import ./cfg.nix { };
    in
    {
      nixosConfigurations = {
        # configuration for each "machine"
        nixwrk =
          let
            settings = {
              stateVersion = "24.11";
              system = "x86_64-linux";
            };
          in
          nixpkgs.lib.nixosSystem rec {
            pkgs = import nixpkgs { # inherit the nixpkgs and modify its properties
              system = settings.system;
              config.allowUnfree = true;
            };
            system = settings.system;
            specialArgs = { # these args will be passed as input parameters to each module
              inherit inputs cfg;
              stateVersion = settings.stateVersion;
              hostPlatform = settings.system;
              hostname = "nixwrk";
              user = cfg.user;
              home = cfg.home;
            };
            modules = [ # each separate module handles a specific list of settings (reusable too)
              inputs.nixos-hardware.nixosModules.framework-16-7040-amd
              ./hosts/fw16/disks.nix
              ./hosts/fw16/boot.nix
              ./hosts/fw16.nix
              ./hosts/global.nix
              ./modules/kvm.nix
              ./modules/locale.nix
              ./modules/plasma.nix
              { programs.nix-ld.enable = true; }
              # home-manager configs
              home-manager.nixosModules.home-manager
              {
                home-manager.useGlobalPkgs = true;
                home-manager.useUserPackages = true;
                home-manager.backupFileExtension = "bak";
                home-manager.sharedModules = [
                  inputs.plasma-manager.homeModules.plasma-manager
                ];
                home-manager.users.${cfg.user} = (
                  import ./home/home.nix { # this file defines a function to build user configs
                    inherit inputs cfg;
                    stateVersion = settings.stateVersion;
                    # and here i specify what parameters will be used for these configs
                    configuration = import ./hosts/fw16/config.nix { inherit inputs cfg pkgs; };
                  }
                );
              }
            ];
          };
        # other machines ...
      };
    };

}

Yeah, yeah… I know… it’s not your most advanced configuration, and in fact some things could have done better/easier (if you do know how – tell me). But the key is – it works! And it’s fairly configurable and usable too. If I need to tweak the hotkey configurations which are then passed to the plasma-manager package (which manages the KDE Plasma customizations), I just store and change them in the cfg.nix file (which is imported into the cfg variable and the passed forward in specialArgs. And so my hosts/fw16/config.nix imports a file called home/modules/plasma.nix (not to be confused with modules/plasma.nix which I import in the flake.nix, which are the global settings which enable the plasma package), which looks something like this:

{ cfg, ... }:

{

  workspace = {
    lookAndFeel = "org.kde.breezedark.desktop";
    cursor = {
      theme = cfg.kdetheme.cursorTheme;
      size = 32;
    };
    iconTheme = cfg.kdetheme.iconTheme;
    wallpaper = cfg.wallpaper;
  };
  
  # ...

}

And so if I need to adjust the theme, I simply modify the kdetheme variable in cfg.nix. Likewise, you can define shortcuts, extra shell functions, shell aliases, git configs, etc. All in one place, and because I import it in my flake.nix, this automagically propagates to all the other systems I use this same flake file. Neat.

#

Where It Breaks (for me)

NixOS is brilliant… until it runs into the parts of my life that aren’t Nix-shaped. Here’s where the veneer cracks. From least crucial and more picky to more critical.

#

Nix meets non-Nix

My day job lives on HPC clusters that don’t run Nix and won’t let me bring system-level magic. To feel comfortable working in their shells, I do need to occasionally configure my own .zshrc files, neovim configs, git variables, environment modules, etc. Before, when I used simple old-school .dotfiles repo while source-ing specific files I need on specific machines – managing this was easy enough. Now that my .zshrc, .zfunctions and .gitconfig are all managed by Nix, I do have to keep two separate versions. Annoying, not world-ending though. But it gets worse…

#

Sandboxing vs modern build systems

Nix’s “no network during build” rule is philosophically sound and practically… spicy. Toolchains like Wails (and cousins in the Node/Go/Electron multiverse) assume the internet mid-build. Nix assumes no. You can vend all dependencies ahead of time and patch build steps, but for ecosystems designed around live downloads, packaging can range from tedious to impossible. Turning off the sandbox “just this once” defeats the point; keeping it on means yak-shaving fetchers and patch phases until you question your hobbies.

#

Dev envs are great – until they aren’t

devenv, nix develop, nix-shell are glorious for isolated, project-pinned toolchains. Until a package you need isn’t in nixpkgs (or is out of date). Then you’re writing a derivation, massaging mkDerivation, sprinkling patches, faking FHS when something insists on /usr/bin, and hoping your rpath incantations summon the right glibc. If everyone used Nix, this would be a utopia. In today’s world, compatibility with non-Nix stuff is where weekends go to die. Poorly maintained and legacy frameworks (hello, AMD/ROCm, Intel/SYCL!) – yeah… not a chance. You can make it work, don’t get me wrong, at the cost of your sanity though. And some of you who’re reading this are probably thinking, it can’t be that bad, these are multi-billion dollar corporations, how bad and incompatible can their codebase be. Boy, do I have some bad news for you.

#

Choose-your-own-adventure

Hot take, don’t crucify me, but I don’t want this much “freedom” on day one; I want an authoritative path. Do I use flakes? Channels? nix profile or nix-env (wtf are these anyway)? nix develop or nix-shell (or god forbid maybe devenv? The answers exist—scattered across wikis, blogs, RFCs, gists, and five YouTube channels (and most of the time is either inconclusive or boils down to “pick your poison”). As an end-user trying to work, I want a boring, official “here’s the default stack in 2025” doc and a tidy mental model. I’ll color outside the lines later. It sort of reminds of the whole javascript frameworks situation; there have been three new frameworks introduced designed to substitute them all while I was writing this (haven’t actually checked). But in js that’s kinda understandable – there is really no central authority (apart from the language standards), it’s just a language after all. Nix, on the other hand, is an operating system whose ambition is (at least if feels like it) to depose them all. And, don’t get me wrong – I love it. But sometimes just a little less democracy and a bit more authority could go a long way (I mean, just look at Arch: don’t want to update packages to unstable versions? too bad).

#

Epilogue

Nix is wildly ambitious – and I love where it’s going. It gives me a kind of safety I haven’t felt on any other OS: rollbacks that actually roll back, state I can read and reason about, and the confidence that my machine is exactly what I said it is. That’s rare, and frankly addictive.

But there are still potholes. The non-Nix world is big, and development can hit time-sucking edges: sandboxed builds clashing with modern toolchains, packaging gaps that send you spelunking through derivations, and clusters that don’t care how elegant your flake.lock is. When those seams show, you pay in weekends.

The other big miss is the on-ramp. I don’t need infinite choice on day one; I need a boring, blessed path from zero to “productive,” with flakes/home-manager/profiles explained in one authoritative place. The pieces exist – the narrative doesn’t. I hope that changes.

Still: I’m staying. Nix makes my daily driver feel trustworthy, and that alone is worth the trade-offs. If the ecosystem keeps smoothing the rough edges – and someone finally writes the straight-through manual – it’ll be hard to recommend anything else.

cd ~ gg