NixOS Developer Setup: System Packages vs Home Manager vs devShells

The three-layer mental model

When people migrate to NixOS from a traditional distro, the biggest conceptual shift is that “installing software” now means at least three different things depending on where you put it. Getting this wrong leads to a bloated system config or dev tools bleeding between projects.

The layers are: the system (everything in environment.systemPackages), the user environment (Home Manager), and per-project shells (flake devShells). Each has a clear mandate.

System packages: stay minimal

System packages live in /etc/nixos/configuration.nix and are available to every user on the machine before any session starts. That makes them the right place for tools with no project or user scope.

Reasonable candidates: curl, wget, htop, git, basic network diagnostics, hardware-management tools. A fallback terminal if you need one before your user environment loads.

What does not belong there: your editor, your shell plugins, your dev toolchains. Putting Emacs in environment.systemPackages is not broken, but it bypasses Home Manager’s ability to manage its config declaratively alongside the binary. Same logic applies to zsh.

Home Manager: your user environment

Home Manager is where Emacs, zsh, oh-my-zsh, fonts, and most user-facing tools should live. The key difference from system packages is that it manages packages and their dotfiles together in one declaration.

Zsh and oh-my-zsh

Home Manager has built-in oh-my-zsh support. No separate install, no manual symlink into your home directory:

programs.zsh = {
  enable = true;
  ohMyZsh = {
    enable = true;
    plugins = [ "git" "z" "fzf" ];
    theme = "robbyrussell";
  };
};

Home Manager writes the config and links it on rebuild. Your existing plugin list ports over directly.

Emacs and Doom

For plain Emacs, programs.emacs.enable = true in Home Manager handles the binary. You can also manage your init files declaratively via home.file.

For Doom Emacs specifically, the cleanest current option is nix-doom-emacs-unstraightened. It ships its own Home Manager module and builds your Doom config and its Emacs package dependencies entirely through Nix — reproducible across machines. Add it as a flake input, include its module, then configure:

programs.doom-emacs = {
  enable = true;
  doomDir = ./doom;  # path to your doom.d directory
};

Your LaTeX setup for org-mode and Beamer fits naturally at this layer too. Add texlive.combined.scheme-medium (or a custom TeX scheme) to home.packages. Since you use it across projects rather than inside one, Home Manager is the right level for it.

Dev shells: isolation per project

This is where NixOS earns its reputation for someone juggling Java, Scala, and Haskell. Each project gets a flake.nix with a devShells.default attribute. Running nix develop drops you into an isolated shell with exactly the tools that project needs. Nothing bleeds over.

A minimal Java devShell:

devShells.default = pkgs.mkShell {
  buildInputs = [ pkgs.jdk21 pkgs.maven ];
};

For Haskell, the haskell-flake module is the community-maintained starting point. It handles GHC, Haskell Language Server, and project dependencies without manual pinning.

For Scala, a devShell with pkgs.sbt, pkgs.metals, and a JDK covers most setups. The scala-seed flake template is a functional base to clone and adapt.

Add direnv to your Home Manager config and a per-project .envrc containing use flake. The dev shell activates automatically when you enter the directory. No manual invocation needed.

Migrating gradually

You do not have to get all three layers right on day one. A practical sequence: start with a minimal environment.systemPackages, bring in Home Manager for zsh and Emacs next, and migrate your Doom config into nix-doom-emacs-unstraightened once you are comfortable with flakes. Doom also runs cleanly managed outside Nix during the transition — just point Nix’s Emacs binary at your existing ~/.doom.d while you find your footing.

Get the devShell habit started early, even on simple projects. That is where the difference from Debian is most immediate.

Sources

Similar Posts