↓ Skip to main content

NixOS (7): Where to Install Software — System, User and Project Environments

·2014 words·10 mins
Jin Li
Author
Jin Li
Fate lies within the lightcone.
NixOS Series - This article is part of a series.
Part 7: This Article

Motivation
#

The earlier posts introduced software installation through environment.systemPackages. NixOS also provides other ways to install or use software, including Home Manager, nix shell, nix run and nix develop. They all provide software environments, but serve different uses.

In my earlier Python environment management post, I described several environment tools. NixOS software management presents a similar issue: without a consistent approach, system, user and project environments can become mixed together.

Here I summarize common software installation and environment management methods in NixOS, explain their uses, and give some recommendations. When asking AI to install software, its purpose also helps specify whether to change the system configuration, user configuration or project environment.

Prerequisites
#

  • An existing NixOS flake configuration that builds successfully;
  • Basic familiarity with NixOS modules, as introduced in post 6;
  • An existing Home Manager integration for the Home Manager section.

The configurations below are simplified examples and contain no private configuration. The project development environment uses a separate example directory.

Software Installation and Environment Management Methods
#

The methods can be grouped by where the software is used:

  1. System packages: managed through NixOS configuration, for users of the computer.
  2. User packages: managed through Home Manager, for personal tools and program settings.
  3. Temporary environments: provided by nix shell or nix run, for occasional tools.
  4. Project environments: managed through a project’s devShell, for interpreters, compilers and project dependencies.

These methods can be used together. For example, the system can retain common tools while individual projects provide their own development environments.

System Package Management
#

Installing System Packages
#

If the computer’s users should all have some common commands available, put them in a NixOS module:

1
2
3
4
5
6
7
8
{ pkgs, ... }:
{
  environment.systemPackages = [
    pkgs.curl
    pkgs.jq
    pkgs.ripgrep
  ];
}

This shows the relevant part of a module; keep the existing configuration. The command provided by ripgrep is called rg, so a package name does not always match its command. Once the configuration is applied, these tools are available in the system environment.

Whether a package belongs in a common file still depends on which computers need it. Shared diagnostic tools can go in a common module; software needed on one computer can stay in its host configuration. Post 3 covers that split.

I recommend keeping tools here when they are needed regularly. Occasional software can be used in a temporary environment to keep the system package list manageable.

Enabling System Services
#

For example, enabling the SSH service uses:

1
services.openssh.enable = true;

The module prepares the software and service configuration. Note that adding pkgs.openssh to the package list does not enable the SSH service. To accept connections from other computers, enable the service and check its authentication and access settings.

Some desktop programs have programs.* modules that handle system integration as well as software. When you need that functionality, check the module before adding a package directly. The NixOS manual’s package management chapter describes the available approaches.

Managing User Software with Home Manager
#

Home Manager can manage a user’s software and settings. For example, in an existing Home Manager module:

1
2
3
4
5
6
7
8
9
{ pkgs, ... }:
{
  home.packages = [
    pkgs.jq
    pkgs.ripgrep
  ];

  programs.fzf.enable = true;
}

home.packages adds packages to this user’s environment. programs.fzf.enable uses Home Manager’s fzf module, which can also integrate with enabled shell modules. These options belong in a Home Manager user module; they cannot simply be pasted into an ordinary NixOS module.

The examples repeat jq to demonstrate two installation methods; in practice, choose one suitable place. Home Manager can keep tools related to personal shell or editor settings together with their configuration.

When Home Manager is integrated as a NixOS module, it is normally applied through nixos-rebuild. A standalone installation uses its own home-manager switch. Putting something in user configuration therefore does not necessarily avoid a system rebuild; that depends on the integration. See the Home Manager manual for installation and usage.

Home Manager can also manage configuration files. The next post will cover organizing user configuration and handling existing files when first adopting it.

Temporary Software Environments
#

Using nix shell
#

Suppose you only want jq to inspect a JSON file. Start a shell with the tools available:

1
2
3
4
nix shell nixpkgs#jq nixpkgs#ripgrep
jq --version
rg --version
exit

The first command prepares the packages and adds them to PATH in a new shell. exit returns to the previous shell environment. Trying a tool does not require editing and rebuilding the system configuration.

For one command, you can skip the interactive shell:

1
nix shell nixpkgs#jq --command jq --version

In nixpkgs#jq, nixpkgs resolves through the flake registry, and #jq selects the package. That input need not match the Nixpkgs in your system’s flake.lock. The short form is convenient for trying tools; a reusable environment should lock its input in the project.

“Temporary” refers to this shell’s environment. Downloaded packages remain in the Nix store, with collection depending on references that retain them. Files a program writes to your home or project directory also remain after the shell exits.

Using nix run
#

To launch a program directly, you can use:

1
2
nix run nixpkgs#hello
nix run nixpkgs#hello -- --greeting='Hello from Nix'

Here, hello is a demonstration program. The first command runs it directly. In the second command, arguments after -- are passed to hello to specify its output text.

For a package, nix run chooses its main executable using metadata and names. It does not select an arbitrary command from the package. If a package contains several programs or lacks a suitable default entry point, nix shell ... --command ... can be clearer. See the nix run documentation for the selection rules.

Note that nix shell and nix-shell are different commands. The nix-shell -p jq command common in older tutorials and project files named shell.nix belong to another approach. This series continues to use flakes.

Managing Project Development Environments with devShell
#

Projects often need a particular interpreter, libraries and command-line tools. Creating the environment manually each time repeats those dependency choices and makes it easier to miss something on another computer.

A project can define its development environment in its own flake, as a devShell. Its inputs can differ from the system configuration’s inputs. Updating project dependencies does not require updating the whole computer too.

Creating the Project and Configuration
#

We will provide Python with NumPy, along with jq. Start in a new example directory:

1
2
3
mkdir -p ~/projects/nix-env-demo
cd ~/projects/nix-env-demo
git init

Create flake.nix:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
  description = "A small Python development environment";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

  outputs = { nixpkgs, ... }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" ];
      forAllSystems = nixpkgs.lib.genAttrs systems;
    in
    {
      devShells = forAllSystems (system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in
        {
          default = pkgs.mkShell {
            packages = [
              pkgs.jq
              (pkgs.python3.withPackages (ps: [ ps.numpy ]))
            ];
          };
        }
      );
    };
}

This is a complete demonstration flake. If a project already has flake.nix, add the relevant outputs to it rather than replacing the file.

systems lists two Linux architectures, and genAttrs creates an attribute for each. legacyPackages.${system} selects the package set for that architecture. Despite its name, using legacyPackages this way is common in flakes and does not mean returning to an older configuration style.

devShells.<system>.default is the default development environment, prepared by pkgs.mkShell. packages lists its tools. python3.withPackages provides Python with NumPy already included, so it can be imported after entering the environment. The architecture and packages need to suit your computer. See the Nixpkgs documentation for mkShell parameters.

Creating and Using the Development Environment
#

After saving the configuration, run:

1
2
3
4
git add flake.nix
nix flake lock
git add flake.lock
nix develop

New files must enter Git’s index so Nix can read them in this local Git flake. nix flake lock locks the input currently selected by the branch. Commit both flake.nix and flake.lock when sharing the project. The branch keeps moving, while the lock file keeps this project on its selected input.

Inside the development environment, try:

1
2
3
python -c 'import numpy; print(numpy.__version__)'
jq --version
exit

No exact version is shown here because it depends on the locked input. For a single check, run this from the project directory:

1
nix develop --command python -c 'import numpy; print(numpy.__version__)'

nix develop prepares a development/build environment, beyond adding executables to PATH. This mkShell mainly provides tools; a compiled project can also declare libraries and build dependencies. The nix develop documentation covers more uses.

The environment is not a container. You still have access to your files, network and existing environment, and programs can modify files. Locking tool versions is useful, but it does not automatically fix the project’s source, external data or every runtime result.

Python Dependency Management
#

The withPackages approach suits this small example, whose dependency exists in Nixpkgs. There is no need to run sudo pip install into the store-based Python environment. Nix manages those store files; they are not suitable for manual package installation.

If a project already uses venv, uv or another Python dependency tool, it can keep that workflow and use Nix for the interpreter, compilers and system libraries. Dependencies resolved separately by those tools still need their own locking and installation. flake.lock does not manage them all. Prebuilt Python extensions may also require work on library compatibility.

Depending on the project, Nix can manage Python packages directly or provide the base environment while Python tools manage dependencies. Their lock files cover different parts of the environment, which should be recorded separately.

Troubleshooting
#

Software Versions and PATH
#

A command can come from the system environment, a user environment or the current development environment. When something looks wrong, check what your shell actually selected:

1
2
3
4
type -a jq
command -v jq
readlink -f "$(command -v jq)"
jq --version

type -a shows multiple candidates and can reveal aliases or functions. command -v shows the selected entry. When it returns an executable path, readlink can resolve it to the corresponding store path.

If the version differs from what you expect, check PATH and the shell startup files. Shell configuration can change PATH, and activating a Python virtual environment can select another interpreter. After adjusting the configuration, open a fresh terminal and check again.

Several references to the same store path do not duplicate the package’s contents. Versions produced from different inputs or builds can coexist. That is useful, but it still matters which executable your shell chooses.

Recommended Usage#

Based on the uses above, I recommend choosing as follows:

UseWhere it belongs
Common tools needed by the computer’s usersenvironment.systemPackages
System services or features needing system integrationThe corresponding NixOS module
Personal tools and program settingsHome Manager
An occasional commandnix shell or nix run
Project interpreters, compilers and dependenciesThe project’s own devShell

Other methods include nix profile install, which changes a user’s profile and retains software across sessions until removed. It does not automatically declare that software in the configuration repository. If Home Manager already records your personal environment, distributing the same lasting tools across another installation list adds work.

Software missing from Nixpkgs, packages needing different build arguments, or programs expecting a conventional Linux filesystem may need packaging, override/overrideAttrs, or a container. These programs need additional work on their build or runtime environment. They can still be placed in system, user or project configuration according to their use. The previous post’s mkForce controls module option priority; changing a package build is a different operation.

When using AI to help install software, specify its purpose and configuration location. For example, ask it to “add NumPy to this project’s development environment”. After the change, enter that environment, check the version and run a simple test to confirm the configuration works.

The next post will introduce Home Manager configuration organization and the considerations for managing existing dotfiles.

NixOS Series - This article is part of a series.
Part 7: This Article