Back to Compendiums

My Nix-based setup for macOS

Using Nix for reproducible, declarative management of packages, dotfiles, and system settings on macOS

by matsjfunke

In the following, I'll try to explain my approach to and experience with configuring my Mac using Nix.

The Nix package manager and its ecosystem, including NixOS, were developed by Eelco Dolstra, starting around 2003 as a research project at Utrecht University for his PhD, focusing on functional package management for reproducible software.

With it you can manage your entire development environment, including packages, dotfiles, and system settings, in a reproducible and declarative way. In the following I'll walk through setting up macOS with Nix and Home Manager.

flake.nix is the entry point for the Nix flake (which is a collection of Nix packages and configurations), it defines the inputs and outputs of the flake.

inputs are the dependencies of the flake in my case nixpkgs, nix-darwin and home-manager.

nix
1  inputs = {
2    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
3    nix-darwin = {
4      url = "github:LnL7/nix-darwin";
5      inputs.nixpkgs.follows = "nixpkgs";
6    };
7    home-manager = {
8      url = "github:nix-community/home-manager";
9      inputs.nixpkgs.follows = "nixpkgs";
10    };
11  };
12

outputs are the results of the flake, in this case darwinConfigurations for mac users:

nix
1darwinConfigurations."matsjfunke" = mkDarwinConfig "matsjfunke";  # work
2darwinConfigurations."matsfunke" = mkDarwinConfig "matsfunke";    # private
3

As you can tell by my .dotfiles I love ricing my setup to tailor it perfectly to my workflow. Using Nix I could define configurations in the nix language for example my shell aliases:

nix
1{
2  home.shellAliases = {
3    ls = "ls -la";
4    rbnix = "sudo darwin-rebuild switch --flake ~/.dotfiles/nix#\$USER";
5  };
6}
7

But I didnt want to migrate my dotfiles all to nix but I can still manage my symlinks with Home Manager which is a Nix-based tool for managing user environments, mkOutOfStoreSymlink creates "live" symlinks to your dotfiles in home.nix:

nix
1{
2  config,
3  pkgs,
4  lib,
5  username,
6  ...
7}:
8
9let
10  homeDir = "/Users/${username}";
11  dotfilesDir = "${homeDir}/.dotfiles";
12in
13{
14  home.username = username;
15  home.homeDirectory = homeDir;
16
17  # Home Manager release version
18  home.stateVersion = "24.11";
19
20  # Let Home Manager manage itself
21  programs.home-manager.enable = true;
22
23  # Symlinks (all relative to ~)
24  home.file = {
25    ".zshrc".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/zsh/.zshrc";
26    ".gitconfig".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/git/.gitconfig";
27    ".ssh/config".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/ssh/config";
28    ".wezterm.lua".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/wezterm/.wezterm.lua";
29    ".config/nvim".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/nvim";
30    ".config/htop/htoprc".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/htop/htoprc";
31    ".config/karabiner/karabiner.json".source =
32      config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/karabiner/karabiner.json";
33    "Library/Application Support/com.mitchellh.ghostty/config".source =
34      config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/ghostty/config";
35  };
36};
37

Replacing Homebrew with Nix means package installation is now handled natively and reproducibly within my configuration. With Nix I can declare packages in code:

nix
1home.packages = with pkgs; [
2  bat
3  coreutils
4  delta # git-delta
5  fastfetch
6  gh
7  git-lfs
8  htop
9  jq
10  neovim
11  ripgrep
12  rsync
13  tree
14  wget
15  doppler
16  gemini-cli
17  ollama
18  openvpn
19  postgresql_15
20  python312
21  tree-sitter
22  nodejs_22
23  ngrok
24  terraform
25  nixfmt
26  pnpm
27  graphicsmagick
28  ghostscript
29  nmap
30];
31

Before switching to it, I used homebrew to install software on my mac, and while I loved that basically every CLI or GUI App is available on homebrew it annoyed me that it's not truly reproducible since brew bundle (brew bundle dump --file=./Brewfile) does not pin versions.

With Nix all packages are pinned to exact versions via nixpkgs commit hashes with a lockfile (flake.lock).

But as of now not all packages and apps are available as a Nix package but using nix-darwin which brings NixOS-style system configuration to macOS, I can still install (unversioned) homebrew packages:

nix
1# Homebrew (only for apps not available in nixpkgs)
2homebrew = {
3  enable = true;
4  onActivation = {
5    cleanup = "zap"; # Remove unlisted packages
6    autoUpdate = true; # auto-update on rebuild
7  };
8
9  brews = [
10    "openssl@3"
11  ];
12
13  casks = [
14    "1password"
15    "alchemy"
16    "beekeeper-studio"
17    "cursor"
18    "docker-desktop"
19    "emdash"
20    "ghostty"
21    "karabiner-elements"
22    "linear-linear"
23    "postman"
24    "raycast"
25    "slack"
26    "spotify"
27  ];
28};
29

nix-darwin also enables me to declaratively configure macOS system settings that would normally require clicking through System Preferences:

nix
1# Required for user-specific system.defaults
2system.primaryUser = username;
3
4# Let Determinate manage Nix (not nix-darwin)
5nix.enable = false;
6
7# macOS System Settings
8system.defaults = {
9  # Dock
10  dock = {
11    autohide = true;
12    launchanim = true; # Bounce animation when opening apps
13    show-recents = false;
14    tilesize = 48;
15    orientation = "bottom";
16    show-process-indicators = true;
17    wvous-tl-corner = 1;  # Top-left: disabled
18    wvous-tr-corner = 2;  # Top-right: Mission Control
19    wvous-bl-corner = 1;  # Bottom-left: disabled
20    wvous-br-corner = 1;  # Bottom-right: disabled
21  };
22
23  # Finder
24  finder = {
25    AppleShowAllExtensions = true;
26    AppleShowAllFiles = true;
27    ShowPathbar = false;
28    FXEnableExtensionChangeWarning = false;
29    _FXShowPosixPathInTitle = true;
30  };
31
32  # Global
33  NSGlobalDomain = {
34    AppleInterfaceStyle = "Dark";
35    ApplePressAndHoldEnabled = false;
36    AppleShowAllExtensions = true;
37    InitialKeyRepeat = 10;
38    KeyRepeat = 1;
39    "com.apple.trackpad.scaling" = 3.0;
40  };
41
42  # Trackpad
43  trackpad = {
44    Clicking = true; # Tap to click
45    FirstClickThreshold = 0; # Click pressure (0 = light, 1 = medium, 2 = firm)
46  };
47
48  # Loginwindow
49  loginwindow = {
50    GuestEnabled = false;
51  };
52
53  # Require password immediately after sleep
54  screensaver = {
55    askForPassword = true;
56    askForPasswordDelay = 0;
57  };
58
59  # No widgets on desktop
60  WindowManager.StandardHideWidgets = true;
61};
62
63# Sleep settings
64power.sleep.computer = 10;  # Minutes until computer sleeps
65power.sleep.display = 3;    # Minutes until display sleeps
66
67# Allow Touch ID for sudo
68security.pam.services.sudo_local.touchIdAuth = true;
69
70# Fonts (system-wide so all apps like Cursor/Ghostty can find them)
71fonts.packages = with pkgs; [
72  nerd-fonts.d2coding
73];
74

I really enjoy this as it removes the only manual part that I didn't automate with dotfiles prior to using Nix.

Beyond packages and symlinks, Home Manager can manage scheduled tasks via launchd agents (macOS cronjobs):

nix
1launchd.agents.dotfiles-sync = {
2  enable = true;
3  config = {
4    Label = "com.matsjfunke.dotfiles-sync";
5    ProgramArguments = [ "/bin/sh" "-c" "cd ~/.dotfiles && git pull --rebase" ];
6    StartCalendarInterval = [{ Hour = 8; Minute = 0; }];  # Daily at 8:00 AM
7    StandardOutPath = "/tmp/dotfiles-sync.log";
8    StandardErrorPath = "/tmp/dotfiles-sync.error.log";
9  };
10};
11
12launchd.agents.cleanup = {
13  enable = true;
14  config = {
15    Label = "com.matsjfunke.cleanup";
16    ProgramArguments = [ "/bin/sh" "-c" "find ~/Downloads ~/Desktop ~/.Trash -mindepth 1 -mmin +60 -exec rm -rf {} + 2>/dev/null || true" ];
17    StartCalendarInterval = [{ Hour = 2; Minute = 0; }];  # Daily at 2:00 AM
18    StandardOutPath = "/tmp/cleanup.log";
19    StandardErrorPath = "/tmp/cleanup.error.log";
20  };
21};
22

And theres much more you can do with Nix like having Per-project dev shells but the above is all I'm doing for now.

And this setup allows me to setup my entire dev environment with just 4 commands and roughly 2 Minutes:

bash
1# 1. Clone dotfiles
2git clone https://github.com/matsjfunke/dotfiles.git ~/.dotfiles
3
4# 2. Install Homebrew
5/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
6
7# 3. Install Nix (Determinate Systems)
8curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
9
10# 4. Apply configuration (installs brews/casks, CLI tools, creates symlinks)
11sudo darwin-rebuild switch --flake ~/.dotfiles/nix#$USER
12

As you probably guessed, my entire Nix config along with my other configuration files is publicly available in my .dotfiles repo on github.