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
Table of Contents
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
- A system with the Nix Package Manager installed
- Some dotfiles to migrate (optional)
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!