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.
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.
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 };
12outputs are the results of the flake, in this case darwinConfigurations for mac users:
1darwinConfigurations."matsjfunke" = mkDarwinConfig "matsjfunke"; # work
2darwinConfigurations."matsfunke" = mkDarwinConfig "matsfunke"; # private
3As 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:
1{
2 home.shellAliases = {
3 ls = "ls -la";
4 rbnix = "sudo darwin-rebuild switch --flake ~/.dotfiles/nix#\$USER";
5 };
6}
7But 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:
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};
37Replacing Homebrew with Nix means package installation is now handled natively and reproducibly within my configuration. 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 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];
31Before 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:
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};
29nix-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 = 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];
74I 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.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};
22And 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#$USER
12As you probably guessed, my entire Nix config along with my other configuration files is publicly available in my .dotfiles repo on github.