Image for post nix-home-env
Sep 15, 2022 by Luc Perkins

Building a highly optimized home environment with Nix

A few weeks ago, fresh off of a minor victory in my local dev setup, I tweeted this on a whim:

The reaction—120+ likes and 20+ retweets—was modest by broader Twitter standards but pretty strong for a rather vague tweet about Nix. Today, I’d like to follow up on this tweet and show how I’ve used Nix to streamline my laptop environment in ways that have saved me time and made me substantially more productive across programming languages and platforms—and even jobs.

How I used to do things

Once upon a time, I managed my home environment, across several laptops and one desktop, the way that many devs do nowadays: I maintained a series dotfiles (for Vim, tmux, and others) that I updated now and then as needs changed. For executables, I used brew install whenever the need arose (I’m a macOS user almost exclusively).

Whenever I needed to change machines—new work laptop, new present to myself—I eagerly looked forward to building an environment I liked from scratch and in an ad-hoc way, usually with the help of my dotfiles Git repo and some kludgy shell scripts. This approach always more or less “worked” but it was always heavy on time and cognitive effort. The words “reproducible” and “declarative” weren’t yet on my radar, at least in this domain, but my longing for them certainly was. And then a piercing ray of light came into my life.

Enter Home Manager

When I first came upon Nix eight years ago, I was intrigued but it seemed like something for hardcore Linux folks and hardly something that could revolutionize my daily practices. That all changed when I discovered Home Manager and began using it devoutly. Home Manager is a Nix-based tool for managing home environments in a declarative, reproducible way. It enabled me to toss out the tangled mess of dotfiles and shell scripts I previously relied upon in favor of one repo—and not a terribly complex one at that—where I could declare my entire environment with due precision: installed executables, Vim, tmux, and Visual Studio Code configuration, and much more.

Home Manager provided me with a qualitatively better way to do things. I liked it so much that I wrote about my journey from Homebrew to Home Manager on my personal blog. I wouldn’t dream of going back to my pre-Home-Manager life. But as you’ll see, Home Manager was not the end of the story for me.

From Home Manager to Nome

After a few years as a happy Home Manager user, I decided to get more ambitious and turn what I’ve learned about Nix and home environments into a concrete project. So I created a project called Nome , which stands for Nix Home (I was also born in Alaska and have fond memories of one of my first books, the Gnome from Nome ). Nome is a highly customized Nix flake that provides everything that I need for my home environment.

Home Manager is the foundation stone of Nome. I use it to configure Vim, Starship, VS Code, and the rest. But I’ve since gone beyond Home Manager because my needs have changed. Nowadays, I strive to make a clean separation between two things:

  1. My global setup, meaning executables and configuration that I truly need to have available everywhere in my environment.
  2. Project-level setups, where the environment is highly specific and

I now use Home Manager only for my global setup. When it comes to specific projects, I get as granular as possible. Back in the day, I’d kick off a new project with touch Makefile and start defining commands like build, clean, and dev. But now I have a single command for starting new project, proj, which is an alias for this:

Terminal window
nix flake init --template github:the-nix-way/nome

This initializes a Nix flake template that provides a least-common-denominator dev environment consisting of a few toolchains that I use frequently:

Each of these toolchains is just a collection of executables. The Rust toolchain, for example, includes a standard Rust toolchain plus cargo edit, cross, rust-analyzer, and other utilities that I’m likely to need.

Here’s the block of the flake.nix that defines a project’s shell environment:

1
nome.lib.mkEnv {
2
toolchains = with nome.lib.toolchains;
3
devops ++ elixir ++ go ++ kubernetes ++ node ++ protobuf ++ rust;
4
extras = with nome.pkgs; [];
5
shellHook = "";
6
};

I don’t ever need to use all of my toolchains, so I always start by removing the ones I don’t need. Then, if I need any other specific executables, I add those under extras (nome.pkgs is a re-exported Nixpkgs pinned to a specific commit, so I can put just about anything here). If I need commands to be run whenever I initialize the environment, I put those in shellHook. The result: it now typically takes me less than a minute to get a project configured to my exact specifications.

Bringing my environment to a new machine

One of the things I like about the single-project approach I’ve adopted is that it’s rather trivial to port it from machine to machine. Let’s say that I accidentally drop my current laptop into the ocean and have to buy one. Here’s what I would need to do to get that new machine up to speed.

  1. Install Nix and enable Nix flakes
  2. Run these commands:
Terminal window
nix build "github:the-nix-way/nome#homeConfiguration.lucperkins.activationPackage"
./result/activate

This creates the initial Home Manager generation on the machine, along with all of my dotfiles, and installs my desired packages with it. I don’t change machines all that often, but it’s nonetheless consoling to know that the effort I’ve put into Nome will never be for naught—unlike those countless hours lost to manual setup in more benighted times.

Nome and NixOS

Nome is also home to my NixOS configuration. Truth be told, I’m a novice NixOS user, so I only have one configuration that I’ve been slowly building out—and a pretty simple one at that. But thus far it’s been extremely convenient to kick start NixOS on a VM with just one command:

Terminal window
nixos-rebuild switch \
--flake "github:the-nix-way/nome#lucperkins"

Nome’s future

Nome is still very much in its youth but I expect it to be a steady and ever-maturing companion as long as I work in software. I even have a rough roadmap of where I want to take it in the near future:

  1. Refine my NixOS configurations to the point where I can use NixOS as my primary development environment, even on macOS. Mitchell Hashimoto’s illuminating nixos-config project has emboldened me to take this on. I’ll share details on this journey at a later date.
  2. Add a nix-darwin configuration to define lower-level details of my macOS environment.
  3. Refactor my Nix sources to be more modular, so that I can share Nix expressions across, for example, my NixOS configurations, my macOS Home Manager config, and my proj system.
  4. So far my Home Manager configuration assumes my Apple M1 environment (aarch64-darwin in Nix terms) but I’d like to make it more robust by adding a Linux configuration that I can run on a (future) Linux machine.

Nome’s scope

Nome is intended only as a personal project. If you find inspiration in it, great! If you want to git clone it and use it as a template, please feel free. But don’t be surprised if you dislike some of my choices or even my entire approach. My hope isn’t that you follow directly in Nome’s footsteps but rather that it convinces you to see your home environment as your career-long companion and thus worthy of a long-running personal project.


Share
Avatar for Luc Perkins
Written by Luc Perkins

Luc is a technical writer, software engineer, and Nix advocate who's always on the lookout for qualitatively better ways of building software. He originally hails from the Pacific Northwest but has recently taken to living abroad.