NixOS Dev Log 001

In dev log 000, we installed NixOS and got started with some simple configurations for software we installed as systemPackages. The natural next stop will be to begin working towards creating a configuration for the environment that I want in as reproducible and well organized a manner as we can manage. To help the system feel more familiar, I want want to install my terminal emulator of choice (ghostty), my shell configurations of choice (oh-my-zsh with the starship prompt), and neovim as my standard text editor.

At first, I installed all of these as individual packages as system packages, and was configuring them in the configuration.nix. But as I started installing more software, it was clear that a single configuration file would become quite unruly with certain programs (think tons of zsh aliases, or a massive neovim configuration). Outside of NixOS, we typically think of managing software through dotfiles – and while you can certainly use dotfiles with NixOS, you need your nix store to be aware of them for the system to be fully reproducible. To this end, I begin looking into home manager.

Home Manager

Home manager is a NixOS tool that allows you to declaratively manage your home environment on NixOS. With home manager, you can split out your package installations and configurations to a specific home manager file. You can even separate out more involved configurations into their own modules that you then import into your home manager file, which I’ll illustrate in a future dev log when I discuss my neovim configuration.

To this point, we’ve been rebuilding our system with the standard documented command:

sudo nixos-rebuild switch

We need sudo permissions to rebuild the system, and nixos-rebuild is the standard command for rebuilding the system based on your updated configurations. The switch directive ensures that after rebuilding your system will be switched to the latest derivation of your system. NixOS stores previous generations of your system, and it’s simple to roll-back to previous generations. It’s a good idea to generally clean up the nix store as it accumulates bloat over time from various iterations of your system and various projects that you run. You can garbage collect unreachable objects in your /nix/store with:

$ nix-collect-garbage
...
deleting '/nix/store/w5bg14xdmqrqycdibf7s2v79v3kg7cgp-pathspec-0.12.1.tar.gz.drv'
deleting '/nix/store/36m5ykrr7b3af0ra574rciicbj6nlphm-source.drv'
deleting '/nix/store/jangf7kxvylqv5z049v63lkbyw80d21m-source.drv'
deleting '/nix/store/4gmazxhcy08bxwrbjwlwwd58z9q961qm-source.drv'
deleting '/nix/store/1gqzah3vd033d28255pbmxc4vs3i2yj0-source.drv'
deleting '/nix/store/s3340x9k8fgmdyvmdx3wdl1k1nvxpirh-fantomas.6.3.16.nupkg.drv'
deleting '/nix/store/ry4xlfrvcd8j5qlfw4dcz5zxm2jsn6gq-pnpm-10.17.1.tgz.drv'
deleting '/nix/store/hxrc40klqdnkzy88cwhx9xawlcv1rp8k-init.lua'
deleting '/nix/store/0sa1v2ncma4zrq228pb6nrzwa1v0iarh-python3.13-ruamel-yaml-clib-0.2.12'
deleting '/nix/store/5nxvsnw0q39q7q3ip0spr63h33a6adip-source.drv'
deleting '/nix/store/qz4z395xq22xyr9i78jyy6w9cxamnlb3-xvidcore-1.3.7.tar.bz2.drv'
deleting '/nix/store/hb1pycm4i454z9amvsivpmvpvgajwi84-outcome-1.3.0.post0.tar.gz.drv'
deleting '/nix/store/sapkfjm3dpgqy6z5gnvq3kpbsq6ijw2l-source.drv'
deleting '/nix/store/l508f0m9by9qs42a1s89zf4l5sgvy826-crates-nvim-ac9fa498.drv'
deleting '/nix/store/qs23hk8k9jkmxj63b98zzdsahv276qgg-source.drv'
deleting '/nix/store/l6fybqq4n0lfhyd7gnyz0929c57jbpar-brotli-1.1.0'
deleting '/nix/store/31za5596pag5bg3fi28z2hsfzldjzahj-warp-3.4.8.tar.gz.drv'
deleting '/nix/store/glvqzjam7l2j3zz5vdk0xdwhj3fwxyna-source.drv'
deleting '/nix/store/1v6aynh6i7g55s43kaf89v0frlrvm6gq-tzdata-2025.2.tar.gz.drv'
deleting '/nix/store/qylw5ql11ibq7xwrka9xwbk6s0pswsbb-sniffio-1.3.1.tar.gz.drv'
deleting '/nix/store/a0nc18rfdgrds1ndpjzpr22qwqwsad8d-stage-2-init.sh'
deleting '/nix/store/b6vwvljd1yib6lq7z4v1gc2x2n36x0pq-source'
deleting '/nix/store/2awa4yfb2bn98bm8jkjv17qm36751z9s-source.drv'
deleting '/nix/store/529hfyqnbmdxqnjjwbn97x4jaji1lg0m-crate-curl-0.4.46.tar.gz.drv'
deleting '/nix/store/ha4llcnsj4n3hg7is6dz83ih2sh8c6yy-nspr-4.37.tar.gz.drv'
deleting '/nix/store/5cf7rh51iyk37rxabil691l5ai535s7m-astor-0.8.1.tar.gz.drv'
deleting '/nix/store/lw1gi4aw0h1yq73fwlg66v28smjcnb61-blaze-html-0.9.2.0.tar.gz.drv'
deleting unused links...
note: currently hard linking saves -0.00 MiB
6543 store paths deleted, 2026.39 MiB freed

To get home manager changes to stick, you have to run home-manager switch to get things setup after you rebuild your system, which isn’t very ergonomic (2 commands to get the system state updated is 1 too many). There is also a matter of keeping our overall system up to date – right now, we’re just specifying packages we want, configuring them, and only installing the latest from the channels we’re using. For more flexibility, we’ll rely on flakes.

Flakes

Nix Flakes are an experimental feature of nix that make it easier to create reproducible nix expressions. Flakes can be used directly with the nix package manager, and I’ve used them across various projects – this site itself uses a nix flake to help manage cross platform development.

Flakes can also be used to manage your NixOS configuration. Specifically, I setup my flake to manage the channels that my configuration are coming from, as well as other inputs to my build system.

{
  description = "Nixos config flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, ... }@inputs: {
    nixosConfigurations = { 
      framework-desktop = nixpkgs.lib.nixosSystem {
        specialArgs = {inherit inputs;};
        modules = [
          ./hosts/framework-desktop/configuration.nix
          inputs.home-manager.nixosModules.default
        ];
      };
    };
  };
}

Breaking this down, let’s look at the inputs that are being consumed to produce our outputs. For our first input, we specify our nix channel – this specifies the unstable branch from the main NixOS repository. The “unstable” branch is the bleeding edge, and with this channel, you do run the risk of breakages for the benefit of not having to wait 6 months for a new stable branch – I personally prefer to have more frequent updates. The second input is our home-manager channel, which we also indicate to follow the same nix packages as our flake to avoid duplication via inputs.nixpkgs.follows = "nixpkgs";.

For outputs, we specify a nixosConfiguration that specifies framework-desktop, which takes in specialArgs that inherit inputs, and also takes in 2 modules: configuration.nix and our home manager module.

Now, to rebuild the system and also have home manager configurations take effect, I can use a modified command:

sudo nixos-rebuild switch --flake /home/leon/nixos-config/#framework-desktop

Now, when going to rebuild the system, I also specify the flake with --flake /home/leon/nixos-config/#framework-desktop. The name of the flake is based on the output from our flake.nix under the nixosConfigurations attribute set. When we define other hosts/machines in the future, we could specify them directly in the flake and then those systems could be rebuilt accordingly by pointing to the right flake.

The pinned nix flake packages can be updated via sudo nix flake update.

Home Manager Configurations

Now that we are using home manager, I can begin to move my configurations there. To enable that, I need to point to my home manager module from my configuration.nix:

home-manager = {
    extraSpecialArgs = { inherit inputs; };
    users = {
      "leon" = import ./home.nix;
    };
  };

In home.nix, which was generated by home-manager, I can specify my packages and their configuration. Previously I mentioned how I use zsh – I can configure that now in home manager. First I specify it in my packages:

home.packages = [
		...
    pkgs.zsh
    ...
];

Then I can specify my specific configuration:

programs.zsh = {
    enable = true;
    autosuggestion.enable = true; # Enables auto-suggestions
    syntaxHighlighting.enable = true; # Enables syntax highlighting
    shellAliases = {
      update = "sudo nixos-rebuild switch --flake /home/leon/nixos-config/#framework-desktop";
    };
    oh-my-zsh = {
      enable = true;
      plugins = [];
      theme = "robbyrussell";
    };
  };

While I’ve configured my shell here, to have it activated for my user, I need to specify that in my configuration.nix for my user:

  users.users.leon = {
    shell = pkgs.zsh; # Specify my shell of choice
    isNormalUser = true;
    description = "Aumit Leon";
    extraGroups = [ "networkmanager" "wheel" ];
    packages = with pkgs; [
    ];
  };

Config Structure

.
├── flake.lock
├── flake.nix
├── hosts
│   └── framework-desktop
│       ├── configuration.nix
│       ├── hardware-configuration.nix
│       └── home.nix
├── LICENSE
├── modules
│   └── home-manager
│       └── nvf.nix
└── README.md

5 directories, 8 files

I have a hosts sub-directory that has the various machines I will support, which includes my standard configuration.nix, as well as my home manager file (home.nix). Right now, I’m only running NixOS on my framework machine, but as I define new hosts, I can also share more configurations between them by moving shared configurations to the modules/ sub directory and importing them accordingly. You may also notice nvf.nix under modules – more on this in a future dev log.

Conclusion

We now have a system that is managed by flakes and uses home manager for more modular configuration. From here, we have basically everything we need to build out a configuration for our development environment. We ultimately want to be able to run our configuration on different machines, which we’ll specify as distinct hosts with shared modules later on. In future dev logs, I’ll discuss how I evolved my system configuration, how AI assisted this evolution, and how we’re continuing to trend towards a development environment so good we can and want to run it everywhere.