---
title: "Declarative management of dotfiles with Nix and Home Manager"
url: "https://www.bekk.christmas/post/2021/16/dotfiles-with-nix-and-home-manager"
authors: ["Ole Kristian Pedersen"]
published: "2021-12-16"
updated: "2025-06-24T20:59:24Z"
language: "en-US"
type: "article"
tags: ["Sky"]
keywords: ["nix", "configuration", "declarative configuration", "home manager", "nix flakes", "nix 2.4", "flakes", "dotfiles"]
---

# Declarative management of dotfiles with Nix and Home Manager

Nix is a declarative package manager with rising popularity. One of the best ways to actually learn it is to use it, and what better way to learn it than using it every day to manage your dotfiles?

## In short

Nix and Home Manager can make dotfiles and development tools declarative, reproducible, and easy to sync across machines. The article shows how to install packages, manage config files, lock dependencies with flakes, and migrate gradually from Homebrew to Nix-based setups.

- **For:** Developers who want to manage dotfiles and local tooling with Nix and Home Manager
- **Good to know beforehand:** Basic familiarity with the command line, git, and dotfiles

_AI-generated_

I've spent quite some time configuring my development environment to make the tools I use daily work well together. I have multiple computers and want similar configurations on each computer. In the beginning, copying files between computers worked okay-ish, but keeping the computers in sync was hard. So I started looking for alternatives.

I started using a "dotfiles" repository as the single source of truth to keep track of my configuration files. I used different shell scripts, symbolic links and package managers to keep the configuration the same. The process was still fragile and required a lot of work to be sure configuration files and programs stayed in sync on all my computers. Small differences in per-system configuration could wreak havoc when I tried to move it to a new computer.

Late 2020 was the first time I was introduced to Nix. I found some time over Christmas holidays to get started with some simple tutorials and navigating the ecosystem. I then slowly migrated my dotfiles from Homebrew to Nix (using Home Manager, I'll get back to that) whenever I had time.

What's the benefit of using Nix for dotfiles? Moving to Nix gave me a declarative, reproducible setup, with built-in automation and flexibility. I also did not have to deal with dead symlinks and long bash scripts. The setup on a new computer was also faster and more reliable than previous approaches.

In the rest of this article I'll explain how I started replacing my configuration, and walk you through the first steps to learning Nix by using it on your everyday computer.

## Getting started

This blog post won't do a deep dive into the benefits of Nix, since the excellent blog post [_Deterministic systems with Nix_](https://www.bekk.christmas/post/2021/13/deterministic-systems-with-nix) published a couple of days ago already introduces the key concepts. If you haven't read that blog post, now is a good time to do so and read the rest of this article afterwards.

### Installing Nix 2.4

The [recommended way of installing Nix](https://nixos.org/manual/nix/unstable/installation/installing-binary.html) is:

```
$ sh <(curl -L https://nixos.org/nix/install)
```

Since [Nix 2.4 was released recently](https://discourse.nixos.org/t/nix-2-4-released/15822), we'll use the new feature called "Flakes", which helps us with a better way to handle dependencies. I'll not go into details about Nix Flakes and how it works, because [this 3-part series](https://www.tweag.io/blog/2020-05-25-flakes/), by the creator of Nix himself, is the best resource to get an introduction to Nix Flakes. We'll need to create the file `~/.config/nix/nix.conf`, with the following line to enable it:

```
experimental-features = nix-command flakes
```

## Installing Home Manager

Finally, we'll use the tool [Home Manager](https://github.com/nix-community/home-manager). It's created to to manage the user environment (e.g., dotfiles) in a declarative way using Nix. We'll start out by adding the minimal configuration needed to get Home Manager up and running.

Start by creating a `dotfiles` directory if you don't have one already (the name of the directory does not matter). Use `git init` (or your favorite GUI) to initialize a git repository in the folder. Add the two following files:

`flake.nix`, follow the `TODO` comments in the file to make it work on your system:

```nix
{
  description = "My Home Manager flake";

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

  outputs = inputs: {
    defaultPackage.x86_64-linux = home-manager.defaultPackage.x86_64-linux;
    defaultPackage.x86_64-darwin = home-manager.defaultPackage.x86_64-darwin;
 
    homeConfigurations = {
	    # TODO: Modify "your.username" below to match your username
      "your.username" = inputs.home-manager.lib.homeManagerConfiguration {
        system = "x86_64-darwin"; # TODO: replace with x86_64-linux on Linux
        homeDirectory = "/home/your.username"; # TODO: make this match your home directory
        username = "your.username"; # TODO: Change to your username
        configuration.imports = [ ./home.nix ];
      };
    };
  };
}
```

`home.nix` (we'll edit this more later):

```nix
{ ... }: {
  programs.home-manager.enable = true;
}
```

Use `git add` (or your favorite GUI) to stage these files. No need to commit, but in order to use Nix Flakes, all files referred to (directly or indirectly) by the flake must be tracked by git (i.e., at least staged).

Finally, running `nix run . switch` installs `home-manager` in your environment. We'll get back to exactly why this command works at the end of the blog post. Confirm that Home Manager is installed and available by running `home-manager help`.

The first run will also create a lock file, `flake.lock`, which locks flake dependencies to a given commit in the flake repository. I recommend adding this file to version control to make sure your configuration is reproducible across systems.

## Using Home Manager to install packages

Okay, let's install `cowsay` using Home Manager. We'll edit the `home.nix` file to contain the following:

```nix
{ pkgs, ... }: {
   programs.home-manager.enable = true;

   home.packages = [
    pkgs.cowsay
  ];
}
```

Then, run `home-manager switch`. This command downloads whatever is required, does some magic with symbolic links, and you should now be able to run `cowsay`:

```
$ cowsay "Hello bekk.christmas!"
 _______________________
< Hello bekk.christmas! >
 -----------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
```

Okay, so how do you uninstall it? Easy. Just remove the program from your `home.nix`, and then run `home-manager switch` again. `home.nix` should now look something like this:

```nix
{ pkgs, ... }: {
  programs.home-manager.enable = true;

  home.packages = [
  ];
}
```

Okay, so right now we've seen than Nix and Home Manager the same features as Homebrew: we can install and uninstall packages.

But wait, there's more! We can manage program configurations using Home Manager too.

## Using Home Manager to manage program configuration

What does "manage program configuration" mean, really? Okay, so dotfiles are not just programs, but configuration files too. For instance, you might have a `.bashrc` file for your shell, or `.gitconfig` to manage your git preferences. We can create and/or manage these files with Home Manager. For instance, you can add your git configuration to `home.nix` like this:

```nix
{ pkgs, ... }: {
  programs.home-manager.enable = true;
  
  home.packages = [
  ];
  
  programs.git = {
    enable = true;
  };
}
```

Running `home-manager switch` will install git, and create a (currently empty) configuration file at `~/.config/git/config`. Also note the similarity between the way we install Home Manager, and the way we installed git - they are equivalent.

NB!  Home Manager will try not to overwrite files and symlinks not managed by Nix, so if you already have a configuration file in the same location, `home-manager` will exit with an error:

```
Existing file '/home/your.username/.config/git/config' is in the way of '/nix/store/m42x1cc0g10cis1w58mbgdkkyn8pv1w1-home-manager-files/.config/git/config'
Please move the above files and try again or use 'home-manager switch -b backup' to back up existing files automatically.
```

Following the help text in the error message and running, e.g., `home-manager switch -b bk`, will copy the pre-existing configuration file to `~/.config/git/config.bk`.

We can use Home Manager to add some basic configuration to the configuration file like this:

```nix
  programs.git = {
    enable = true;
	  includes = [
      { path = "~/.gitlocalconfig"; }
    ];
    aliases = {
      ap = "add -p";
    };
    extraConfig = {
      pull.ff = "only";
    };
  };
```

Running `home-manager switch` again, you should now get the following content in `~/.config/git/config`:

```
[alias]
        ap = "add -p"

[pull]
        ff = "only"

[include]
        path = "~/.gitlocalconfig"
```

One important thing to note: you're not allowed to edit the newly created configuration file! Why? Because the file is supposed to be immutable, and can only be changed by updating your dotfiles and running `home-manager switch` again. The benefit is that you don't get any configuration drift, but on the flip side, the new workflow can be hard to get used to.

The Home Manager wrapper around program configuration is not perfect. It doesn't have all the options for all programs, and might not be that flexible compared to the original configuration format. Usually, an option named `extraConfig` or similar is available to use configurations options that are not supported by the wrapper. Additionally, not all programs are wrapped by Home Manager. Check out [Appendix A of the Home Manager manual](https://nix-community.github.io/home-manager/options.html) to see all the available configuration options for wrapped programs.

Another possibility is to install the program directly as a package, and use `home.file.<name>.source` to use files directly. As an example, let's use the Git configuration example above. Store the same configuration in a file called `gitconfig` in your `dotfiles` directory (remember to stage it with `git add` too!):

```
[alias]
        ap = "add -p"

[pull]
        ff = "only"

[include]
        path = "~/.gitlocalconfig"
```

Then, modify `home.nix` to install git as a package, and source the new `gitconfig` file:

```nix
{ pkgs, ... }: {
  programs.home-manager.enable = true;
  
  home.packages = [
    pkgs.git  # Install git as a pacakge
  ];
  
  # Install the gitconfig file, as .gitconfig in the home directory
  home.file.".gitconfig".source = ./gitconfig;
}
```

After running `home-manager switch`, you should have the file `~/.gitconfig` and the previous config file (`~/.config/git/config`) should be removed.

Note that the new file is still not editable. You still have to use the same workflow of editing it in your `dotfiles` repository, followed by `home-manager switch`. This works just like the previous method (which is good!), but you can keep configuration files on their original format.

Okay, so we've now learned a couple of ways to install software and manage configuration files using Nix and Home Manager. You know more or less everything you need to move most of your installed tools away from Homebrew or any other package manager you're using. How do you use your new knowledge in practice?

## Moving from Homebrew to Nix and Home Manager

Start simple, and learn as you go. I started out moving tools with no dependencies and little to no configurations, such as `ripgrep`, `curl`, `fzf`, `git`, etc. Use the `home.file.<name>.source` approach for configuration files to get them quickly managed with Nix and Home Manager. Remember to uninstall programs from your other package manager. Some programs might not be supported by Nix on your platform, just skip those for now.

You can use [this page](https://search.nixos.org/packages?channel=unstable) to find the correct name of the packages you want to install. Some packages do not have the same name in all package managers. This page is easier to use than the alternatives and shows packages available on NixOS, but not all of them is available on e.g., macOS. You can use `nix search nixpkgs <name>` to search for packages available on your platform.

From here you can go multiple ways to improve your configuration. Trying to move configurations from files into Home Manager (using the `programs.<program-name>` approach) is a good way to get more acquainted the Nix syntax and Home Manager. [Appendix A of the Home Manager manual](https://nix-community.github.io/home-manager/options.html) contains all the available configuration options for each program.

Other ways to move forward include trying to port programs not available on your platform using [overlays](https://nixos.wiki/wiki/Overlays) or trying to use your configuration on another computer. You could also try installing GUI programs like VS Code, a terminal or other programs you're using.

As you go, you might want to split your configuration in multiple files to easier manage it. You can create new Nix files and import them like in the example below:

```nix
imports = [
  ./my-new-file.nix
  ./my-second-file.nix
]
```

This is all you need to get started. Happy learning! Just to help you out a bit more, I've tried to answer most of the common issues I encountered below:

**How do I update my installed programs?**

You can update all you programs by navigating to your `dotfiles` folder and running `nix flake lock`, which updates the `flake.lock` file. Then run `home-manager switch`.

**How do I install programs not supported on my system?**

[Overlays](https://nixos.wiki/wiki/Overlays) is a cool feature where you can override a package to make it work for a new system, assuming it exists for a system already. Or you can create your own [derivation](https://github.com/justinwoo/nix-shorts/blob/master/posts/your-first-derivation.md). Overlays and derivations can quickly get complicated when unfamiliar with Nix, so you might want to first check out out the [Nix User Repository (NUR)](https://github.com/nix-community/NUR) which might contain overlays and packages for your system (or give you some good examples to start out from).

**How do I get GUI programs into Spotlight search on macOS?**

Spotlight only indexes programs located in `/Applications/` or `~/Applications/`. There are discussions to figure out how to best solve this, but luckily workaround exists. I use this one (use at your own risk!):


```nix
{ lib, pkgs, config, ... }:
{
  # Copy GUI apps to "~/Applications/Home Manager Apps"
  # Based on this comment: https://github.com/nix-community/home-manager/issues/1341#issuecomment-778820334
  home.activation.darwinApps =
    if pkgs.stdenv.isDarwin then
      let
        apps = pkgs.buildEnv {
          name = "home-manager-applications";
          paths = config.home.packages;
          pathsToLink = "/Applications";
        };
      in
      lib.hm.dag.entryAfter [ "writeBoundary" ] ''
        # Install MacOS applications to the user environment.
        HM_APPS="$HOME/Applications/Home Manager Apps"
        # Reset current state
        [ -e "$HM_APPS" ] && $DRY_RUN_CMD rm -r "$HM_APPS"
        $DRY_RUN_CMD mkdir -p "$HM_APPS"
        # .app dirs need to be actual directories for Finder to detect them as Apps.
        # In the env of Apps we build, the .apps are symlinks. We pass all of them as
        # arguments to cp and make it dereference those using -H
        $DRY_RUN_CMD cp --archive -H --dereference ${apps}/Applications/* "$HM_APPS"
        $DRY_RUN_CMD chmod +w -R "$HM_APPS"
      ''
    else
      "";
}
```

This is based on [this issue comment](https://github.com/nix-community/home-manager/issues/1341#issuecomment-778820334) and a couple of related comments in the same thread.

Save it in a file called `darwin-application-activation.nix`, and import it by using the following snippet in `home.nix`:

```nix
imports = [
  ./darwin-application-activation.nix
];
```

**How does the** **`flake.nix`** **file we're using actually work?**

You've only made slight modifications to the file after importing it, so let's go through the most important parts. The first part is just a human-readable description of the Flake:

```nix
description = "My Home Manager flake";
```

Next up is the inputs. There are two inputs: `nixpkgs` and `home-manager`, both of which point to a GitHub repository containing a `flake.nix` file. Additionally, we make the `nixpkgs` input of the `home-manager` flake point to the same version of `nixpkgs` that we're using in this flake.

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

Finally, the outputs. Let's split them up a little bit. The first part declares `outputs` as a function that takes one argument `inputs`:

```nix
outputs = inputs: {
```

Then we declare a couple of `defaultPackage`s. These point to the default packages of the `home-manager` flake (i.e., the `home-manager` program). These are defined for convenience, so  when we invoke `nix run . switch` on our first run (when `home-manager` is not yet available) it is equivalent to running `home-manager switch`:

```nix
defaultPackage.x86_64-linux = home-manager.defaultPackage.x86_64-linux;
defaultPackage.x86_64-darwin = home-manager.defaultPackage.x86_64-darwin;
```


Last, we define the `homeConfigurations`, which is the output read when we invoke `home-manager`. `home-manager` looks up the user name of the current user, and find the entry in `homeConfigurations` that corresponds to the user name. It also needs some information about your system, and a list of files to import.

```nix
homeConfigurations = {
  "your.username" = inputs.home-manager.lib.homeManagerConfiguration {
    system = "x86_64-darwin";
    homeDirectory = "/home/your.username";
    username = "your.username";
    configuration.imports = [ ./home.nix ];
  };
};
```

If you want to use your configuration on another system where the username is different, you will have to add another entry in `homeConfigurations`.

## Relevant resources recommended by the author

- [Deterministic systems with Nix](https://www.bekk.christmas/post/2021/13/deterministic-systems-with-nix): Introduces the key concepts of Nix
- [Nix Flakes](https://www.tweag.io/blog/2020-05-25-flakes/): A three-part introduction to Nix Flakes, by the creator of Nix
- [Home Manager configuration options](https://nix-community.github.io/home-manager/options.html): A list of all configuration options for programs wrapped by Home Manager
