Manage user environments with Nix and Home Manager

Nix is a functional package manager for Linux and macOS which helps build reproducable setups.

Instead of installing packages globally, it stores each package in the Nix Store. In there, each package is placed in a directory named with a cryptographic hash of the package and all of its dependencies. This hash changes whenever a package is updated, so packages are never overwritten.

For example, installing the fish shell through Nix adds fish the the load path from Nix’s store directory:

/nix/store/dg6z33cy6jqqmclyg8wxx91lkc324lsj-fish-3.6.0/bin/fish

The packages are symlinked to by updating the shell’s load path, which allows for running shells with different versions of a program, or even spinning up a shell with a previously uninstalled program available.

After following the install instructions, use the nix run command to run a program with Nix. For example, to try the fish shell:1

nix run nixpkgs#fish\
    --extra-experimental-features nix-command \
    --extra-experimental-features flakes

This fetches the fish package from the nixpkgs package registry, downloads it to the Nix Store and starts it:

Welcome to fish, the friendly interactive shell
Type help for instructions on how to use fish
~>

After closing the shell, fish is no longer in the load path:

which fish
fish not found

System configuration with Home Manager

Home Manager is a system for configuring user environments, built on Nix.

Set up a flake with the Home Manager template by running flake new:

nix flake new ~/.config/nixpkgs -t github:nix-community/home-manager
{
  description = "Home Manager configuration of Jane Doe";

  inputs = {
    # Specify the source of Home Manager and Nixpkgs.
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { nixpkgs, home-manager, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      homeConfigurations.jdoe = home-manager.lib.homeManagerConfiguration {
        inherit pkgs;

        # Specify your home configuration modules here, for example,
        # the path to your home.nix.
        modules = [ ./home.nix ];

        # Optionally use extraSpecialArgs
        # to pass through arguments to home.nix
      };
    };
}

Then, update the output system and username and remove the boilerplate:

{
  description = "Home Manager configuration";

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

  outputs = { nixpkgs, home-manager, ... }:
    let
      system = "x86_64-darwin";
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      homeConfigurations.jeffkreeftmeijer = home-manager.lib.homeManagerConfiguration {
        inherit pkgs;

        modules = [
          ./home.nix
        ];
      };
    };
}

For Home Manager’s configuration, create home.nix. It lists your use name and home directory, but also the stateVersion, which determines the Home Manager release the configuration is compatible with. The home.nix file also sets up Home Manager to install and manage itself:

{ config, pkgs, ... }:

{
  home = {
    username = "jeffkreeftmeijer";
    homeDirectory = "/Users/jeffkreeftmeijer";
    stateVersion = "22.11";
  };

  programs.home-manager = {
    enable = true;
  };
}

Finally, install Home Manager and apply the configuration:

nix run ~/.config/nixpkgs#homeConfigurations.jeffkreeftmeijer.activationPackage

Running the activationPackage generates a flake.lock file, which locks all packages to their currently installed versions for reproducability. It only lists Home Manager now, but installed packages will be added to the list when they’re added.

Installing packages

To install a package, add it to home.packages in home.nix:

diff --git a/home.nix b/home.nix
index 6f6f86d..12f9efe 100644
--- a/home.nix
+++ b/home.nix
@@ -5,6 +5,7 @@
     username = "jeffkreeftmeijer";
     homeDirectory = "/Users/jeffkreeftmeijer";
     stateVersion = "22.11";
+    packages = [ pkgs.git ];
   };

   programs.home-manager = {

Then, update the environment by running home-manager switch:

home-manager switch
Starting Home Manager activation
Activating checkFilesChanged
Activating checkLaunchAgents
Activating checkLinkTargets
Activating writeBoundary
Activating copyFonts
Activating installPackages
replacing old 'home-manager-path'
installing 'home-manager-path'
Activating linkGeneration
Cleaning up orphan links from /Users/jeffkreeftmeijer
Creating profile generation 2
Creating home file links in /Users/jeffkreeftmeijer
Activating onFilesChange
Activating setupLaunchAgents

After switching, the newly-installed package is available and symlinked to in the ~/.nix-profile directory:

which git
/Users/jeffkreeftmeijer/.nix-profile/bin/git

  1. The nix run command relies on the nix-command and flakes features. Both of these are currently experimental and disabled by default, but they’re enabled using the --extra-experimental-features flag. To enable these features globally, set experimental-featurs in nix.conf:

    mkdir -p ~/.config/nix
    echo "experimental-features = nix-command flakes" >> ~/.config/nix/nix.conf
    
    experimental-features = nix-command flakes
    
    ↩︎