Stop using nix-env.

For the sake of you and others.

nix-env was built as a tool for Nix as a way to manage packages in a traditional (imperative) fashion. It tries to bridge the gap between the imperative and declarative world. A replacement for the venerable "just sudo apt install <anything>". As a result of its design, it often causes unexpected behaviour. This page is dedicated to explaining what its issues are and what to use instead.

Installing packages by derivation name

Packages in Nix are usually bundled in attribute sets. Each attribute name represents the name of a package. When installing packages declaratively through NixOS or Home Manager, or when declaring a package's dependencies, these are uniquely identified using their attribute name. When installing packages via nix-env -i, attribute names are ignored. Instead, nix-env traverses the entire attribute set to find a package with a matching derivation name. This can lead to fun surprises when the derivation name does not match the attribute name, such as installing an unwrapped package that requires a wrapper to function properly. This can be avoided by using nix-env -iA instead, which picks packages via attribute name, but does not note down from which attribute path a package originally came from, resulting in surprises when upgrading it.

    
{
  pkg = < derivation pkg-wrapper-1.3 >;

  pkg-unwrapped = < derivation pkg-1.3 >;
}
    
  

Name collisions when upgrading packages

nix-env -u will upgrade all packages in your profile by searching through the attribute set for a derivation with the same derivation name and a higher version number. When using large, nested package collections like nixpkgs, derivations from different language ecosystems may be stored under a distinct attribute path, but their derivation name may be the same, despite the two packages being clearly different otherwise.

    
{
  zstd = < derivation zstd-2.0 >;

  haskellPackages = {

    zstd = < derivation zstd-3.0 >;

  };
}
    
  

Unintentional major version jumps

Nixpkgs sometimes keeps multiple major versions for packages that have multiple continuously maintained release trains, such as PostgreSQL. Because the distinction between major versions is done via attribute names, nix-env completely ignores it. When installing postgresql_12 via its attribute name, you would expect PostgreSQL to stay at major version 12, even when upgraded. Even though postgresql_12 and postgresql_14 may exist in parallel within the same nixpkgs revision, nix-env ignores this fact and will happily upgrade your PostgreSQL package to major version 14.

Performance issues

As you may have already guessed, iterating over a large package set and evaluating every derivation in it is not very efficient.

Non-obvious shadowing

nix-env installs packages into a user-specific profile that has precedence over system-level directories in $PATH. This means that you can install a different version of any tool in your user profile without the rest of the system having to use that version. If you forget that you installed a program into your user profile like this, you may end up with a nasty surprise later down the line.

For example, installing busybox this way would shadow common utilities such as ls, cat or grep. The next time you would try to use a GNU-specific feature of grep months down the line, you might end up going on a wild goose chase to figure out why your version of grep behaves differently than everyone else's.

Implicit package pinning

Packages installed by nix-env are independent of the NixOS or Home Manager configuration. This can be beneficial because NixOS upgrades will never change what versions of packages a user has installed, however it is a common source of confusion, as a system update ("apt upgrade") on regular distros will truly upgrade all packages, while on NixOS or when using Home Manager, this is not the case.

Informational Video by Matthew Croughan

Matthew Croughan demonstrates some of the issues with nix-env.

Alternatives

So, nix-env is bad. What do we use instead? First, it's important to consider that Nix is a far more powerful package manager than your garden variety ones like APT or DNF. As such, there is more than one way to provide a package. Merely installing the package is probably never your end goal. Consider what you want to do with this package, and then choose how to expose the package in the target environment appropriately.

Declarative package management

This is the best choice for any packages you expect to be long-living. Any applications that you commonly use should be managed in this way. Packages can be managed declaratively through tools such as NixOS configuration or Home Manager. These will often also provide a set of options to configure the application in Nix code. System services should only be configured through these options, which will automatically define a suitable systemd service according to your specifications.

Ephemeral shell environments

Do you often run into a situation where you need a particular command for a one-off thing, but don't feel like it should reside on your system at all times? Ephemeral shells allow you to gain temporary access to a command and after you exit out of the shell, it's as if the package was never installed. If you're using Flakes, nix shell may be more up your alley.

nix profile

Lastly, nix profile from the Nix 3.0 (Flakes) set of CLI commands aims to provide a more polished imperative package management solution. If you really need to imperatively manage some of your packages, this is your best option. It picks packages by attribute name rather than derivation name and it keeps track of the attribute path from which each package was installed, meaning name collisions when upgrading are eliminated. Thanks to Flakes, it also allows you to easily install packages from package collections other than nixpkgs.

Version 1.2.1 | This is a living document. | Contributions welcome