In my previous post, I showed how to use Nix flakes for project-specific development environments. The same tool can also configure your entire system, including packages, shell setup, editor configuration, and even macOS system preferences.
Instead of maintaining dotfiles, brew lists, and setup scripts across multiple repos, everything lives in one place and can be applied with a single command.
The Structure
My configuration lives at ~/.config/nix
:
nix-config/
├── flake.nix # Entry point
├── common/ # Shared across all systems
│ ├── packages.nix # CLI tools
│ ├── zsh/ # Shell configuration
│ ├── neovim/ # Editor setup
│ ├── tmux/ # Terminal multiplexer
│ └── claude-code/ # AI assistant integration
└── systems/
├── aarch64-darwin/ # macOS-specific
│ ├── homebrew.nix # GUI applications
│ └── defaults.nix # System preferences
└── aarch64-linux/ # Linux-specific
└── home-linux.nix
The Main Flake
Here's the flake.nix
that ties everything together:
{
description = "Cross-platform Nix configuration";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nix-darwin = {
url = "github:LnL7/nix-darwin";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nix-darwin, nixpkgs, home-manager, ... }: {
# macOS configuration
darwinConfigurations = {
"rick" = nix-darwin.lib.darwinSystem {
system = "aarch64-darwin";
modules = [
./common
./systems/aarch64-darwin
home-manager.darwinModules.home-manager
{
home-manager = {
useGlobalPkgs = true;
useUserPackages = true;
users.svenlito = import ./systems/aarch64-darwin/home.nix;
};
}
];
};
};
# Linux configurations
homeConfigurations = {
morty = home-manager.lib.homeManagerConfiguration {
pkgs = import nixpkgs { system = "aarch64-linux"; };
modules = [ ./systems/aarch64-linux/home-linux.nix ];
};
};
};
}
Shared Package Management
Instead of maintaining separate package managers, I have one file that defines core CLI tools:
# common/packages.nix
{ pkgs, ... }: {
home.packages = with pkgs; [
# Modern CLI replacements
eza # ls replacement
bat # cat replacement
ripgrep # grep replacement
fd # find replacement
zoxide # cd replacement
# Dev tools
gh # GitHub CLI
lazygit # Git TUI
tmux # Terminal multiplexer
neovim # Editor
];
}
This gets included in both macOS and Linux configurations. Same tools, identical versions, everywhere.
Platform-Specific: macOS GUI Apps
On macOS, I use Homebrew for GUI applications:
# systems/aarch64-darwin/homebrew.nix
homebrew = {
enable = true;
casks = [
"orbstack" # Docker replacement
"ghostty" # Terminal
"raycast" # Spotlight replacement
"1password"
"linear"
"slack"
"spotify"
"notion-calendar"
];
onActivation = {
autoUpdate = true;
cleanup = "zap"; # Remove unlisted casks
};
};
Even GUI apps are declarative and version-controlled.
macOS System Preferences
System preferences can be configured as code:
# systems/aarch64-darwin/defaults.nix
system.defaults = {
dock = {
autohide = true;
show-recents = false;
tilesize = 48;
};
NSGlobalDomain = {
AppleShowAllExtensions = true;
InitialKeyRepeat = 15;
KeyRepeat = 2;
};
finder = {
AppleShowAllExtensions = true;
ShowPathbar = true;
FXEnableExtensionChangeWarning = false;
};
};
Applying the Configuration
On macOS:
# Clone the config
git clone https://github.com/svnlto/nix-config ~/.config/nix
cd ~/.config/nix
# Apply (auto-detects hostname)
nixswitch
On Linux:
# Clone the config
git clone https://github.com/svnlto/nix-config ~/.config/nix
cd ~/.config/nix
# Apply (auto-detects hostname - make sure hostname matches a config in flake.nix)
home-manager switch --flake .#$(hostname)
That's it. One command installs and configures:
- All CLI tools
- Shell environment (zsh, aliases, prompt)
- Editor setup (Neovim with LSP)
- Terminal multiplexer (tmux)
- GUI applications (macOS)
- System preferences (macOS)
Custom Commands
I've added helper commands in my zsh config:
# systems/aarch64-darwin/home.nix
shellAliases = {
# macOS system rebuild (auto-detects hostname)
nixswitch = "sudo darwin-rebuild switch --flake ~/.config/nix#$(scutil --get LocalHostName)";
# Update and rebuild
nix-upgrade = "nix flake update ~/.config/nix && nixswitch";
# Linux home-manager rebuild (auto-detects hostname)
hmswitch = "home-manager switch --flake ~/.config/nix#$(hostname)";
hm-upgrade = "cd ~/.config/nix && nix flake update && hmswitch";
};
Now nixswitch
rebuilds my entire macOS system, and hmswitch
handles Linux configurations.
Rollback Support
Every change creates a new generation. If something breaks:
# On macOS
sudo darwin-rebuild rollback
# On Linux
home-manager generations # List available versions
home-manager switch --switch-generation 42
Atomic rollbacks for system configuration.
Configuration Management Workflow
My typical workflow:
- Edit config files in
~/.config/nix
- Run
nixswitch
(macOS) orhmswitch
(Linux) - Changes apply immediately
- If something breaks, rollback to the previous generation
All changes are in git, so I can also revert commits if needed.
Why This Works
Using the same tool for both project and system configuration means:
- One language (Nix) for all environment management
- Version-controlled system configuration
- Reproducible setup across machines
- Easy onboarding (clone repo, run one command)
- Atomic rollbacks if anything breaks
The flake.nix
in each project defines project dependencies. The flake.nix
file in ~/.config/nix
defines system dependencies: the same patterns, the same tooling, but different scopes.
Getting Started
- Install Nix:
sh <(curl -L https://nixos.org/nix/install)
- Enable flakes in
~/.config/nix/nix.conf
:
experimental-features = nix-command flakes
Fork or clone a starter configuration (like mine)
Customise for your needs
Run
nixswitch
(macOS) orhmswitch
(Linux)
My full configuration: github.com/svnlto/nix-config
Top comments (0)