My Nix-based setup for macOS
Using Nix for reproducible, declarative management of packages, dotfiles, and system settings on macOS
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:
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};
26As 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:
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];
16And 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:
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};
33nix-darwin also enables me to declaratively configure macOS system settings that would normally require clicking through System Preferences:
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];
71I 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):
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};
11And 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:
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
12As you probably guessed, my entire Nix config along with my other configuration files is publicly available in my .dotfiles repo on github.