How to Manage Your Dotfiles the Nix Way with Home Manager

Associated Youtube Video: https://youtu.be/IiyBeR-Guqw

Associated Odysee Video: https://odysee.com/@LibrePhoenix:8/manage-your-dotfiles-with-home-manager!:9

Previous Tutorial: Intro Flake Config Setup for New NixOS Users

Next Tutorial: Methods of Making Your NixOS Config More Modular

So what is Home Manager?

Home Manager allows declarative management of your dotfiles and user-level applications, and doesn’t require root to operate. Just like the rest of NixOS, home-manager makes distinctive “generations” of everything every time you switch.

All of the dotfiles in the current home-manager configuration are then generated (as symlinks) and if you want to quickly try out an entirely different set of dotfiles, you can do that! Switching into a completely different configuration will clean up all of the old dotfiles and only activate the new ones!

Tools Needed

Installing Home Manager: 2 Ways

Here is the Home Manager Install Guide. The guide describes two different methods of installing, the standalone installation and the NixOS module installation. Here are some key differences:

Standalone installation NixOS Module
home-manager (user config) and configuration.nix (system config) are totally separate Everyone’s dotfiles are managed by the system in configuration.nix
Easy to take your home-manager dotfiles and transfer them to a system you don’t have admin priveleges to Much easier to manage dotfiles for a multi-user system using this method

Standalone Installation

In the case of the standalone installation, the system-level stuff and user-level stuff are separate. The sudo nixos-rebuild command along with the configuration.nix file are for system-level stuff, while the home-manager command and home.nix file are for user-level stuff. These are separate.

NixOS Module Installation

This is different from the standalone installation. In this setup, everything is managed inside the configuration.nix and everything is rebuilt using sudo-nixos-rebuild. The advantage here is that multiple users can be configured and managed simultaneously, but the disadvantage is that users without root access have less autonomy in configuring via home-manager.

I use the Standalone Installation

I prefer the standalone installation, since it ensures a clear demarcation between “system-level” stuff and “user-level” stuff. This does create a slight bit of extra overhead, but this is what I prefer. As such, this is the method I will be discussing in this post. Some information in this post will apply to both the standalone installation and NixOS module installation, such as the section about configuration options.

How to install?

For the standalone method, you can refer to the instructions in the Home Manager Manual.

Start by adding appropriate home-manager channel, according to your version of nixpkgs:

# for unstable nixpkgs:
nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
# for latest stable nixpkgs:
nix-channel --add https://github.com/nix-community/home-manager/archive/release-23.11.tar.gz home-manager
# for xx.xx version of nixpkgs:
nix-channel --add https://github.com/nix-community/home-manager/archive/release-xx.xx.tar.gz home-manager

Then, update the nix channels and install:

nix-channel --update
nix-shell '<home-manager>' -A install

NOTE: Sometimes the nix-shell '<home-manager>' -A install command will fail. All you need to do to fix this is logout and log back in. I think this is because some necessary environment variables (or something similar) don’t update until you log back in.

At this point, you should have access to the home-manager command and your home-manager config file will be at ~/.config/home-manager/home.nix.

Intro Home Manager Config

If you open up ~/.config/home-manager/home.nix, you will notice it bears a resemblance to the system config file at /etc/nixos/configuration.nix. There are a few differences, but it’s mostly the same idea. For example, you can install packages with the home.packages list (just like environment.systemPackages from the configuration.nix):

  # User packages
  home.packages = with pkgs; [
    hello
    cowsay
    emacs
  ];

Just like I discussed in the previous blog post, you can search for available packages on MyNixOS, where anything marked as nixpkgs/package is a package.

Once you’ve edited the home.nix file, you can rebuild your home configuration via the command:

home-manager switch

just like nixos-rebuild switch.

What about flakes?

If you followed my previous guide, you have a flake, which allows better and more precise control of package versions than the default method: Nix channels.

If we want to incorporate the home.nix file with the flake.nix setup, we’ll need to move the home.nix file to where the flake is, and make a few edits to the flake.

Start by moving the home.nix file to the ~/.dotfiles directory (that we made last time). At this point, your ~/.dotfiles directory should look like this:

~/.dotfiles
├── configuration.nix
├── flake.lock
├── flake.nix
├── hardware-configuration.nix
└── home.nix

Next, we’ll update the flake.nix to account for home-manager and the home.nix file.

Incorporating Home Manager into the Flake

The basic flake structure for the overall NixOS system we saw last time is something like this:

{
  description = "My first flake!";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-23.05";
    # use the following for unstable:
    # nixpkgs.url = "nixpkgs/nixos-unstable";
    # or any branch you want:
    # nixpkgs.url = "nixpkgs/{BRANCH-NAME}"
  };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
      };
  };
}

The Inputs Block

We need to add home-manager as an input and we can do so by adding the appropriate home-manager repository like so:

{
  description = "My first flake!";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-23.05";
    home-manager.url = "github:nix-community/home-manager/release-23.05";

    # use the following for unstable:
    # nixpkgs.url = "nixpkgs/nixos-unstable";
    # home-manager.url = "github:nix-community/home-manager/master";

    # or any branch you want:
    # nixpkgs.url = "nixpkgs/{BRANCH-NAME}"
    # home-manager.url = "github:nix-community/home-manager/{BRANCH-NAME}";
  };

  outputs = { ... };
}

The master branch would be for the nixos-unstable branch of nixpkgs, while stable releases are the branches marked as release-xx.xx, like release-23.05 which is the latest stable branch at the time of writing this.

One more thing is necessary for the inputs section, however! home-manager itself takes nixpkgs as an input, so we need to make sure that the version of nixpkgs used by the system is the exact same version used by home-manager. To do this we need to add home-manager.inputs.nixpkgs.follows = "nixpkgs", like this:

{
  description = "My first flake!";

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

  outputs = { ... };
}

That will be everything for the inputs!

The Outputs Block

The outputs section from last time was like this:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
        };
      };
    };
}

This defines some nixosConfigurations by using the lib.nixosSystem function. To add our home-manager configuration, we’re going to define an analogous homeConfigurations using the home-manager.lib.homeManagerConfiguration function, with a structure like this:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, home-manager, ... }:
    let
      lib = nixpkgs.lib;
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
        };
      };
      homeConfigurations = {
        USERNAME = home-manager.lib.homeManagerConfiguration {
          pkgs = DEFINEPKGSHERE;
          modules = [ ./home.nix ];
        };
      };
    };
}

The NixOS configurations are named according to your hostname (YOURHOSTNAME above), since they define the entire system. Analogously, the home configurations are named according to your username (USERNAME above).

The homeManagerConfiguration function takes two arguments. Like the nixosSystem function, it requires some modules. For the system, the main module file is the configuration.nix, and for home-manager it is home.nix.

What’s different and not exactly analagous is that the nixosSystem function takes the system architecture as an input (system), while the homeManagerConfiguration function takes the package set (pkgs) as a function. This is derived from nixpkgs and the architecture type, so we have to specify it in a let binding and inherit it:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, home-manager, ... }:
    let
      lib = nixpkgs.lib;
      pkgs = nixpkgs.legacyPackages.${"x86_64-linux"};
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          system = "x86_64-linux";
          modules = [ ./configuration.nix ];
        };
      };
      homeConfigurations = {
        USERNAME = home-manager.lib.homeManagerConfiguration {
          inherit pkgs;
          modules = [ ./home.nix ];
        };
      };
    };
}

The inherit directive simply passes the pkgs defined in the let binding as the argument (pkgs) required by the homeManagerConfiguration function.

Using this idea and a bit of substitution magic, we can actually declare the system architecture in the let binding as well, which is cleaner:

{
  description = "My first flake!";

  inputs = { ... };

  outputs = { self, nixpkgs, home-manager, ... }:
    let
      system = "x86_64-linux";
      lib = nixpkgs.lib;
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          inherit system;
          modules = [ ./configuration.nix ];
        };
      };
      homeConfigurations = {
        USERNAME = home-manager.lib.homeManagerConfiguration {
          inherit pkgs;
          modules = [ ./home.nix ];
        };
      };
    };
}

Putting it All Together

At this point, your entire flake should be something like this:

{
  description = "My first flake!";

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

  outputs = { self, nixpkgs, home-manager, ... }:
    let
      system = "x86_64-linux";
      lib = nixpkgs.lib;
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      nixosConfigurations = {
        YOURHOSTNAME = lib.nixosSystem {
          inherit system;
          modules = [ ./configuration.nix ];
        };
      };
      homeConfigurations = {
        USERNAME = home-manager.lib.homeManagerConfiguration {
          inherit pkgs;
          modules = [ ./home.nix ];
        };
      };
    };
}

Now that we have it in the flake, we won’t be using the same command to synchronize the configuration, now, while in the directory with your flake, you can use:

home-manager switch --flake .#NAMEOFCONFIGURATION

If your username matches your configuration name, you can drop the last bit:

home-manager switch --flake

Updating the system is still done by calling

nix flake update

while in the directory with the flake.

Remember: updating the flake by itself does not update anything! You must run home-manager switch --flake after updating the flake to fully update your home configuration!

Configuration Options

So this has been quite a bit of overhead, so, how is this actually useful? The power in home-manager lies in its configuration management by using options.

There are tens of thousands of options, and you can easily explore them using the MyNixOS tool again.

When you search, anything designated as home-manager/option is an option you can place into home.nix, just like any option designated as nixpkgs/option can be placed into configuration.nix.

These options can be set, and they affect the resultant configuration files.

An Example Using Options: .bashrc and .zshrc

A good example to start with is configuring bash and/or zsh using home-manager. MyNixOS performs fuzzy searches so a search query like “home-manager/option bash” will result in all of the home-manager options for bash (similar to “home-manager/option zsh”).

Using this you can explore the plethora of options. The most important option is the enable option. If you forget to set enable to true, then the actual dotfiles will not be created.

So to start, you can add the following to home.nix:

{ config, pkgs, ... }:

{
  # ... all your other stuff

  programs.bash = {
    enable = true;
  };
  programs.zsh = {
    enable = true;
  };
}

Since we’re going to be setting multiple options underneath programs.bash, we avoid writing programs.bash.enable = true; and instead write the options in the squiggly brackets ({}).

Next we could try the shellAliases option to define a few aliases:

{ config, pkgs, ... }:

{
  # ... all your other stuff

  programs.bash = {
    enable = true;
    shellAliases = {
      ll = "ls -l";
      .. = "cd ..";
    };
  };
  programs.zsh = {
    enable = true;
    shellAliases = {
      ll = "ls -l";
      .. = "cd ..";
    };
  };
}

In the above code, we repeat the definition of shellAliases across both the bash and zsh config, but we don’t have to do it this way! Instead, we can declare the aliases as a variable, like myAliases using a let binding:

{ config, pkgs, ... }:
let
  myAliases = {
      ll = "ls -l";
      .. = "cd ..";
    };
in
{
  # ... all your other stuff

  programs.bash = {
    enable = true;
    shellAliases = myAliases;
  };
  programs.zsh = {
    enable = true;
    shellAliases = myAliases;
  };
}

I really like zsh! How do I make it the default?

Me too! I use zsh by default! If you want to make zsh the default shell for the system, you can incorporate some of the nixpkgs/options into your configuration.nix! Without going into too much detail, these are the relevant options if you want zsh as the default:

# configuration.nix
{ config, pkgs, ... }:

{
  # ... all your other stuff

  # I use zsh btw
  environment.shells = with pkgs; [ zsh ];
  users.defaultUserShell = pkgs.zsh;
  programs.zsh.enable = true;
}

Now what?

At this point, I can’t tell you how you should configure your system, since it’s your system! I would recommend going to MyNixOS exploring the plethora of options available to you, both for your configuration.nix and home.nix.

If you have a bunch of dotfiles that you want to migrate over from a previous setup like Arch, Gentoo, Fedora, or whatever, it’s totally ok to migrate them slowly! You can still place the dotfiles into their normal locations and they will work just like you’d expect! Those configurations just won’t be managed by home-manager.

Next time, we’ll see how you can leverage tools in the Nix language to make your configuration more modular, which extends everything that we’ve done so far by allowing you to easily switch between multiple different conflicting configurations on the fly!

Donation Links

If you have found my work to be helpful, please consider donating via one of the following links. Thank you very much!