Moving from Homebrew to NixOS
Homebrew keeps a machine's tools in a list you add to over time. NixOS keeps the whole machine in a file you rebuild from. Moving between them is mostly a translation job, and it goes faster if you treat it as one.
Take an inventory
Before installing anything, write down what Homebrew is doing for you today. brew bundle dump writes every tap, formula, cask and Mac App Store app to a Brewfile, which becomes the checklist for the rest of the move.
# Everything installed, as a checklist
brew bundle dump --file=Brewfile
# Only the formulae you asked for, not their dependencies
brew leaves
# Background services Homebrew is running
brew services list
brew leaves matters more than the full list. Dependencies come along automatically in Nix as well, so you only need to translate the packages you chose. In a typical Brewfile that removes well over half the lines.
Map the names
Most command-line tools have the same name in nixpkgs. The exceptions are versioned formulae, where Homebrew uses @ and nixpkgs uses an underscore or a separate attribute. search.nixos.org or nix search nixpkgs <name> settles any doubt.
| Homebrew | nixpkgs | Where it goes |
|---|---|---|
ripgrep, fd, jq, gh | same names | home.packages |
node@22 | nodejs_22 | project dev shell |
python@3.12 | python312 | project dev shell |
awscli | awscli2 | home.packages |
postgresql@16 + brew services | services.postgresql | configuration.nix |
git and its config | programs.git | home-manager |
casks such as visual-studio-code | vscode (unfree) | home.packages |
The third column is the real decision. NixOS gives you three places to put a package, and choosing well keeps the configuration tidy.
- System configuration for services and anything every user needs: databases, Docker, SSH, fonts.
- home-manager for your own tools and dotfiles. It replaces both
brew installand the dotfiles repo you were symlinking by hand. - Per-project dev shells for language runtimes. A project pins its own Node or Python version, so nothing needs to be installed globally.
Start from a flake
A flake pins the exact nixpkgs revision in flake.lock, which is the equivalent of Homebrew never upgrading anything until you say so. This is a minimal one that wires home-manager into the system build, so one command updates both.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
home-manager.url = "github:nix-community/home-manager/release-26.05";
home-manager.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { nixpkgs, home-manager, ... }: {
nixosConfigurations.workbench = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
./configuration.nix
home-manager.nixosModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.matt = import ./home.nix;
}
];
};
};
}
The Brewfile's command-line tools then become a list in home.nix, and tools with configuration get a programs.* module instead of a dotfile:
{ pkgs, ... }: {
home.stateVersion = "26.05";
home.packages = with pkgs; [
ripgrep fd jq gh awscli2 vscode
];
programs.git = {
enable = true;
settings.user.name = "Matt Oddie";
settings.init.defaultBranch = "main";
};
programs.direnv = {
enable = true;
nix-direnv.enable = true;
};
}
Replace brew services with systemd
Everything in brew services list becomes a NixOS service option. These are better than their Homebrew equivalents in one respect: the service, its config file, its user and its data directory all come from the same few lines, so rebuilding a machine brings the whole service back.
{ pkgs, ... }: {
nixpkgs.config.allowUnfree = true; # vscode and friends
services.postgresql = {
enable = true;
package = pkgs.postgresql_16;
ensureDatabases = [ "app_dev" ];
};
virtualisation.docker.enable = true;
users.users.matt.extraGroups = [ "docker" ];
}
Move runtimes into projects
With Homebrew, one version of Node serves every project on the machine. On NixOS each project can carry a small flake with a devShells.default listing what it needs, and direnv loads it when you cd into the directory. Adding use flake to the project's .envrc is the whole setup. Two projects on different Node versions stop being a problem you manage.
If you already use mise or asdf with a .tool-versions file, that keeps working on NixOS too. It's a reasonable halfway step while you convert projects one at a time.
Downloaded binaries won't runNixOS has no /lib64/ld-linux-x86-64.so.2, so prebuilt binaries from release pages, npm postinstall scripts and editor extensions fail with "No such file or directory". Set programs.nix-ld.enable = true; in the system configuration and most of them start working.
What doesn't translate
- Mac App Store entries in the Brewfile have no equivalent. Find the Linux version or an alternative for each.
- Apps that update themselves fight the read-only Nix store. Install them from nixpkgs and let the flake update them, or run them as a Flatpak.
- Taps for niche tools sometimes have no nixpkgs package. Check for a flake in the tool's repository first, since many projects now ship one, and package it yourself only as a last resort.
The new upgrade loop
brew update && brew upgrade becomes two commands. Update the lock file, then rebuild:
nix flake update
sudo nixos-rebuild switch --flake .#workbench
# Something broke? Go back to the previous generation
sudo nixos-rebuild switch --rollback
The rollback is what you couldn't do with Homebrew. Every rebuild is a generation you can return to from the command line or the boot menu, so an upgrade that breaks something takes one command to undo. Keep the flake in Git and work down the Brewfile line by line. Once every entry is either translated or deliberately dropped, the move is done.
Written by Matt Oddie · Markdown version