DEV Community

Sven Lito
Sven Lito

Posted on

Using Nix to Configure Your Entire System

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
Enter fullscreen mode Exit fullscreen mode

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 ];
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

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
  ];
}
Enter fullscreen mode Exit fullscreen mode

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
  };
};
Enter fullscreen mode Exit fullscreen mode

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;
  };
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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";
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Atomic rollbacks for system configuration.

Configuration Management Workflow

My typical workflow:

  1. Edit config files in ~/.config/nix
  2. Run nixswitch (macOS) or hmswitch (Linux)
  3. Changes apply immediately
  4. 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

  1. Install Nix:
sh <(curl -L https://nixos.org/nix/install)
Enter fullscreen mode Exit fullscreen mode
  1. Enable flakes in ~/.config/nix/nix.conf:
experimental-features = nix-command flakes
Enter fullscreen mode Exit fullscreen mode
  1. Fork or clone a starter configuration (like mine)

  2. Customise for your needs

  3. Run nixswitch (macOS) or hmswitch (Linux)


My full configuration: github.com/svnlto/nix-config

Top comments (0)