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.

As you can tell by my .dotfiles I love ricing my setup to tailor it perfectly to my workflow. And using Nix I can automate this configuration even more by declaratively managing my dotfile symlinks with Home Manager which is a Nix-based tool for managing user environments, mkOutOfStoreSymlink creates "live" symlinks to your dotfiles (editable in place) rather than copying to the Nix store:

nix
1let
2  homeDir = "/Users/matsfunke";
3  dotfilesDir = "${homeDir}/.dotfiles";
4in
5{
6  home.username = "matsfunke";
7  home.homeDirectory = homeDir;
8
9  # Home Manager release version
10  home.stateVersion = "24.11";
11
12  # Let Home Manager manage itself
13  programs.home-manager.enable = true;
14
15  # Symlinks (all relative to ~)
16  home.file = {
17    ".zshrc".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/zsh/.zshrc";
18    ".gitconfig".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/git/.gitconfig";
19    ".ssh/config".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/ssh/config";
20    ".wezterm.lua".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/wezterm/.wezterm.lua";
21    ".config/nvim".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/nvim";
22    ".config/htop/htoprc".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/htop/htoprc";
23    ".config/karabiner/karabiner.json".source = config.lib.file.mkOutOfStoreSymlink "${dotfilesDir}/karabiner/karabiner.json";
24  };
25};
26

As a package manager, Nix naturally handles installing software. 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 have a concept of a 'Brewfile lock file' to pin versions.

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];
16

And 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 (managed declaratively)
2homebrew = {
3  enable = true;
4  onActivation = {
5    cleanup = "zap"; # Remove unlisted packages
6    autoUpdate = false; # Don't auto-update on rebuild
7  };
8
9  taps = [
10    "homebrew/bundle"
11    "homebrew/services"
12  ];
13
14  brews = [
15    "gemini-cli"
16    "ollama"
17    "openvpn"
18    "postgresql@15"
19    "python@3.12"
20  ];
21
22  casks = [
23    "alchemy"
24    "beekeeper-studio"
25    "docker-desktop"
26    "emdash"
27    "ghostty"
28    "karabiner-elements"
29    "ngrok"
30    "raycast"
31  ];
32};
33

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 = "matsfunke";
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 = 13; # Bottom-left: Lock Screen
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
60# Sleep settings
61power.sleep.computer = 10;  # Minutes until computer sleeps
62power.sleep.display = 3;    # Minutes until display sleeps
63
64# Allow Touch ID for sudo
65security.pam.services.sudo_local.touchIdAuth = true;
66
67# Fonts (system-wide so all apps like Cursor/Ghostty can find them)
68fonts.packages = with pkgs; [
69  nerd-fonts.d2coding
70];
71

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.matsfunke.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

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#matsfunke
12

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