Using Nix/NixOS for package management on a Mac

8 February 2025

Since buying a new Mac, I wanted to try out a new package management solution. I’ve used Homebrew for as long as I can remember.

What is Nix?

Nix is a powerful, purely functional package manager that allows you to define and manage software environments declaratively. What sets it apart from traditional package managers is that it provides reproducible builds: no more “works on my machine” scenarios. With Nix, your environments are guaranteed to be exactly the same on every system, ensuring stability and consistency across different machines.

It shares many benefits with Docker, but one key advantage of Nix is its ability to configure a system declaratively, whereas Docker is more focused on packaging and containerizing applications.

Some of the benefits I’m most interested in are:

  1. Declarative Configuration
    Being able to have my desired packages listed out in a file feels a lot more ergonomic to me than hunting through the output of Homebrew.

  2. Isolation
    I’m able to set up a set of packages (using a shell.nix) on a per-project basis. No more need for version managers installed globally. It’s made my shell startup time a lot quicker.

Here’s an example of a basic shell.nix configuration for a Python project that sets up a specific version of Python, installs some dependencies, and creates an isolated environment:

# shell.nix

{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
  buildInputs = [
    pkgs.python39
    pkgs.python39Packages.pip
    pkgs.python39Packages.virtualenv
  ];

  shellHook = ''
    export PIPENV_VENV_IN_PROJECT=1
    echo "Welcome to the isolated Python environment!"
  '';
}

In this example:

  • pkgs.python39 ensures the Python version is locked to Python 3.9.
  • pkgs.python39Packages.pip and pkgs.python39Packages.virtualenv make sure pip and virtualenv are available for managing Python packages.
  • The shellHook section can be used to customize the environment, like setting environment variables or displaying a welcome message when the shell is started.

With this configuration, each time I run nix-shell in the project directory, I’ll be placed into a reproducible Python environment, with the same versions and dependencies every time.

Being able to recover my system by rolling back is also interesting, but I’m not really sure how I would use that. Modifying a single config file and reapplying it seems to serve the same outcome for me.

I’ve found the Nix language a bit difficult to grasp, and the documentation isn’t easy to understand for beginners. However, I did manage to get a working setup, which works well for me, and that’s probably good enough for now. I haven’t fully explored creating my own flakes, but that would be useful in the future to be able to share some configuration between my personal and work machines.

Most of my configuration has been done with nix-darwin and Home Manager.

Nix-Darwin: Tailoring macOS to Your Needs

While Nix works well on Linux, Nix-Darwin is an extension of Nix tailored specifically for macOS. It allows you to manage macOS system settings declaratively, including configuring services, user settings, and even the system’s shell environment. Instead of dealing with complex system preferences manually, Nix-Darwin lets me define everything in one configuration file, making it easy to maintain consistency across devices or recover from a system failure.

Home Manager: Personalizing Your Development Environment

Home Manager is a tool that integrates seamlessly with Nix to manage your user-specific configuration files and software. Think of it as a way to manage your dotfiles, shells, editors, and other personal settings in a reproducible way. For me, managing my dev tools like Neovim, Zsh, and Git configurations with Home Manager has been a game-changer. I can easily sync my configuration across multiple machines and roll back changes if something goes wrong. No more wrestling with hidden configuration files or trying to remember all the tweaks I’ve made to my setup.


I’ve still got a long way to go understanding Nix, but I’ve started configuring my work machine to use a shell.nix for different projects, which has made switching between different language versions and tools a lot easier. When working with unfamiliar projects that haven’t already been converted to Docker, it’s made setting up a container to run the application a lot simpler.