Skip to content

Darwin Deployment

This tutorial guides you through deploying a macOS machine using nix-darwin. You’ll understand how darwin differs from NixOS, set up zerotier mesh networking, and learn the patterns that make darwin management effective.

By the end of this tutorial, you will understand:

  • How nix-darwin provides declarative macOS configuration
  • Why darwin uses different patterns than NixOS (and the practical implications)
  • How zerotier mesh networking integrates with darwin via Homebrew
  • The structure of darwin machine configurations in this infrastructure
  • How to deploy, update, and troubleshoot darwin hosts

Before starting, you should have:

60-90 minutes for a complete darwin deployment. Updates take 10-15 minutes once the initial setup is complete.

Understanding darwin in this infrastructure

Section titled “Understanding darwin in this infrastructure”

Before deploying, let’s understand what makes darwin different and why those differences matter.

nix-darwin brings NixOS-style declarative configuration to macOS. It manages system preferences, launchd services, homebrew packages, and more through Nix expressions.

Unlike NixOS, which controls the entire operating system, nix-darwin works alongside macOS. Apple controls the kernel, core system services, and many aspects of the user interface. nix-darwin handles the parts that macOS allows external tools to configure.

The infrastructure includes four darwin machines and four NixOS machines:

Darwin machines:

  • stibnite - crs58’s primary workstation (single user)
  • blackphos - raquel’s workstation (dual user: raquel primary, crs58 admin)
  • rosegold - janettesmith’s workstation (dual user: janettesmith primary, cameron admin)
  • argentum - christophersmith’s workstation (dual user: christophersmith primary, cameron admin)

Key differences from NixOS:

AspectDarwinNixOS
Deploymentdarwin-rebuild switchclan machines update
Secrets (clan vars)Planned (not yet implemented)clan vars
Secrets (sops-nix)sops-nix (user secrets)sops-nix (user secrets)
ZerotierHomebrew caskNix package
Service managementlaunchdsystemd
Disk managementmacOS managesdisko via clan

These differences shape how you approach darwin configuration and deployment.

You might wonder why zerotier uses Homebrew instead of Nix on darwin. The answer involves macOS security architecture.

Zerotier requires:

  • System extension installation (managed by macOS security policies)
  • Network extension entitlements (requires Apple notarization)
  • Persistent launchd service registration

Nix can build zerotier, but it can’t install system extensions or handle the macOS security prompts required for network extensions. The Homebrew cask includes Apple-notarized binaries and proper installer scripts.

This is a pragmatic choice: Homebrew handles the parts Nix can’t, while Nix manages everything else. The activation script installs zerotier via Homebrew and joins the network automatically.

Step 1: Explore the darwin configuration structure

Section titled “Step 1: Explore the darwin configuration structure”

Let’s understand how darwin machines are configured before making changes.

Terminal window
ls modules/machines/darwin/

Each darwin machine has its own directory containing:

  • default.nix - Main configuration importing modules and defining machine-specific settings
  • Machine-specific overrides as needed

Look at a typical darwin configuration:

Terminal window
cat modules/machines/darwin/stibnite/default.nix

You’ll see a structure like:

{ config, pkgs, ... }:
{
imports = [
# Aggregate imports
];
# Machine identification
networking.hostName = "stibnite";
# Homebrew configuration (including zerotier)
homebrew = {
enable = true;
casks = [ "zerotier-one" ];
};
# Activation script for zerotier network join
system.activationScripts.postUserActivation.text = ''
# Zerotier network join logic
'';
# User imports
home-manager.users.crs58 = import ../../../home/users/crs58;
}

Darwin configurations use aggregate imports to pull in related features. These aggregates compose modules by functional area:

Terminal window
ls modules/home/all/

Common aggregates include:

  • aggregate-core - Essential tools everyone needs
  • aggregate-shell - Shell configuration (zsh, starship, etc.)
  • aggregate-development - Development tools (git, editors, etc.)
  • aggregate-ai - AI tooling (claude-code, etc.)

This pattern means changes to an aggregate affect all users who import it, reducing duplication.

Step 2: Verify your machine configuration exists

Section titled “Step 2: Verify your machine configuration exists”

Before deploying, ensure your machine has a configuration.

Terminal window
ls modules/machines/darwin/

If your hostname appears in the list, you have a configuration. If not, you’ll need to create one; see the Host Onboarding Guide.

Terminal window
# Replace 'stibnite' with your hostname
nix build .#darwinConfigurations.stibnite.system --dry-run

A successful dry-run means the configuration is valid Nix. Any errors indicate configuration problems to fix before proceeding.

Darwin deployment differs from NixOS because darwin-rebuild runs on the machine itself rather than being pushed from a controller.

  1. Build: Nix evaluates your configuration and builds derivations
  2. Switch: darwin-rebuild creates a new generation and updates symlinks
  3. Activate: Activation scripts run (user creation, service registration, etc.)
  4. Homebrew: If configured, homebrew packages install or update
  5. Post-activation: Custom scripts run (like zerotier network join)

Each successful activation creates a new “generation.” You can list and roll back to previous generations:

Terminal window
# List recent generations
darwin-rebuild --list-generations | head -10
# Roll back to previous generation
darwin-rebuild switch --rollback

This provides safety: if a change breaks something, you can roll back immediately.

Now deploy the configuration to your machine.

Terminal window
darwin-rebuild switch --flake .#stibnite

Replace stibnite with your hostname.

Or use the convenient just task:

Terminal window
just activate

The just activate command auto-detects your platform and hostname.

If this is a fresh machine without prior darwin-rebuild history, you might need:

Terminal window
# First time on a new machine
darwin-rebuild switch --flake .#hostname --impure

The --impure flag allows access to files outside the flake during initial setup. Subsequent rebuilds shouldn’t need it.

During deployment, observe:

  • Building: Derivations being built or fetched from cache
  • Activating: System changes being applied
  • Homebrew: Cask installations (like zerotier-one)
  • Post-activation: Custom scripts running

If you see errors, note the specific message for troubleshooting.

Zerotier connects your darwin machine to the infrastructure’s private mesh network.

The zerotier network (ID: db4344343b14b903) creates a virtual Layer 2 network across all machines:

┌─────────────┐
│ cinnabar │
│ (controller)│
└──────┬──────┘
┌──────────────────┼──────────────────┐
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│stibnite │ │blackphos│ │ galena │
│ (peer) │ │ (peer) │ │ (peer) │
└─────────┘ └─────────┘ └─────────┘

cinnabar is the zerotier controller, managing network membership. All other machines are peers that connect through the controller.

After activation, zerotier should be installed via Homebrew:

Terminal window
# Check zerotier is installed
which zerotier-cli
# Check the service is running
sudo zerotier-cli status

If zerotier-cli isn’t found, the Homebrew cask may not have installed. Check the activation output for errors, or install manually:

Terminal window
brew install --cask zerotier-one

The activation script should automatically join the network. Verify:

Terminal window
sudo zerotier-cli listnetworks

If the network isn’t listed, join manually:

Terminal window
sudo zerotier-cli join db4344343b14b903

New machines need authorization from the network controller. Contact the network administrator (or if you have controller access):

  1. Log into the zerotier controller web interface
  2. Find the pending member by its zerotier address
  3. Authorize the machine
  4. Optionally assign a managed IP address

Once authorized:

Terminal window
# Check network status (should show "OK")
sudo zerotier-cli listnetworks
# Get your zerotier IP address
sudo zerotier-cli listnetworks | awk '{print $NF}'
# Ping another machine on the mesh
ping cinnabar.zt # If DNS is configured
ping 10.144.x.y # Or use the IP directly

Darwin machines use sops-nix for user-level secrets. Clan vars darwin support is planned but not yet implemented; currently clan vars only operates on NixOS machines.

Terminal window
ls -la ~/.config/sops/age/keys.txt

If the key doesn’t exist, complete the Secrets Setup Tutorial first.

After activation, check that secrets deployed:

Terminal window
ls -la ~/.config/sops-nix/secrets/

You should see your configured secrets as files with restricted permissions.

Terminal window
# View a secret (be careful with sensitive values)
cat ~/.config/sops-nix/secrets/github-token

If secrets aren’t appearing, check:

  1. Your age key is in the correct location
  2. Your public key is in .sops.yaml
  3. The sops configuration in your home-manager module is correct

Let’s confirm everything is working.

Terminal window
# Check the current generation
darwin-rebuild --list-generations | head -3
# Verify system profile
echo $PATH | tr ':' '\n' | grep nix
# Check a nix-installed binary works
which git
git --version
Terminal window
# Check home-manager generations
home-manager generations | head -3
# Verify home-manager profile
ls -la ~/.nix-profile/
Terminal window
# Zerotier connectivity
sudo zerotier-cli listnetworks
ping -c 3 cinnabar.zt
# SSH to another machine (if configured)
ssh cinnabar.zt
  • darwin-rebuild --list-generations shows recent generation
  • Nix-installed tools available in PATH
  • Home-manager configuration active
  • Secrets deployed to ~/.config/sops-nix/secrets/
  • Zerotier connected and authorized
  • Can reach other machines on the mesh

You’ve now deployed a darwin machine from start to finish. Along the way, you learned:

  • nix-darwin provides declarative macOS configuration alongside (not replacing) macOS
  • Homebrew integration handles what Nix can’t (system extensions, notarized installers)
  • Zerotier mesh connects darwin machines to the broader infrastructure
  • sops-nix provides user-level secrets on both darwin and NixOS platforms
  • Generations provide rollback safety for darwin configurations

Now that your darwin machine is deployed:

  1. Customize your configuration by modifying your user module in modules/home/users/

  2. Learn about NixOS deployment if you manage servers. The NixOS Deployment Tutorial covers terranix provisioning and clan deployment.

  3. Review operational procedures in the guides:

  4. Understand the architecture more deeply:

Ensure your development shell is active:

Terminal window
cd /path/to/infra
direnv allow
which darwin-rebuild

Your machine needs a configuration entry. Check modules/machines/darwin/ for your hostname, or create one following the Host Onboarding Guide.

Check if Homebrew itself is working:

Terminal window
brew doctor
brew update

If Homebrew needs installation:

Terminal window
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

macOS security may be blocking the system extension:

  1. Open System Preferences > Security & Privacy > General
  2. Look for blocked software from “ZeroTier, Inc.”
  3. Click “Allow” and restart zerotier
Terminal window
sudo launchctl stop com.zerotier.one
sudo launchctl start com.zerotier.one

Check the specific error message. Common issues:

  • Missing Homebrew (brew not found)
  • Network issues during package download
  • Permission problems (need sudo for some operations)

For comprehensive troubleshooting, see the Host Onboarding Guide.