Packages#

Where packages live, how to search, how to pin them, and how to install software that wasn’t built for NixOS in the first place — Steam, vendor binaries, AppImages, Flatpak.

In this page#

The three scopes#

Scope

File

Persisted in

System-wide

environment.systemPackages

Every user’s PATH.

Per-user (declarative)

users.users.<name>.packages

Only that user.

Per-user (home-manager)

home.packages in home.nix

Only that user, plus dotfile management.

Per-user (imperative)

nix profile install / nix-env -iA

Only that user, but not declarative. Avoid on systems you care about reproducing.

Pick one per concern. Typical split: system packages for CLI essentials + system services; home-manager for everything user-facing (editor config, shell, GUI apps, themes).

Searching#

Web:

CLI:

Classic#

nix-env -qa firefox
nix-env -qa '.*firefox.*' --description

Flakes#

nix search nixpkgs firefox
nix search nixpkgs 'steam-run'

Find which package provides a command:

# once
nix-channel --add https://github.com/nix-community/nix-index-database/archive/main.tar.gz nix-index-database
nix-channel --update

# then
nix-locate bin/gcc

Pinning#

Classic — channels#

Channels are named pointers to a tarball URL. Pin by adding a tarball URL with an explicit commit:

sudo nix-channel --add https://github.com/NixOS/nixpkgs/archive/<sha>.tar.gz nixos
sudo nix-channel --update

Flakes — flake.lock#

Inputs pin automatically:

nix flake update                       # bump all inputs, update flake.lock
nix flake update nixpkgs               # positional (Nix 2.19+); replaces --update-input
nix flake lock --override-input nixpkgs github:NixOS/nixpkgs/<sha>

The positional form (nix flake update <inputName>) replaced --update-input <inputName> in Nix 2.19. If you are on an older installation, the flag form still works; switch to positional once you upgrade.

Commit flake.lock alongside flake.nix. Together they give you bit-identical rebuilds on any machine.

Unfree packages#

Global opt-in:

nixpkgs.config.allowUnfree = true;

Per-package opt-in (preferred — auditable):

nixpkgs.config.allowUnfreePredicate = pkg:
  builtins.elem (lib.getName pkg) [
    "steam" "steam-original" "steam-run" "steam-unwrapped"
    "discord" "spotify" "vscode" "obsidian"
    "nvidia-x11" "nvidia-settings" "nvidia-persistenced"
  ];

One-off for a CLI invocation:

NIXPKGS_ALLOW_UNFREE=1 nix-shell -p spotify --run spotify
NIXPKGS_ALLOW_INSECURE=1 nix-shell -p oldtool --run oldtool

The prebuilt-binary problem#

NixOS has no /usr/lib, no /lib64/ld-linux-x86-64.so.2, nothing at the FHS paths a vendor binary expects. Five tools fix this, in roughly increasing effort:

1. programs.steam — the Steam escape hatch#

Valve’s Steam client ships a 32-bit ELF that dynamically links to a huge pile of libraries. nixpkgs wraps it in its own Steam runtime + FHS layer via a module:

programs.steam = {
  enable = true;
  remotePlay.openFirewall = true;
  dedicatedServer.openFirewall = true;
  gamescopeSession.enable = true;
  extraCompatPackages = with pkgs; [ proton-ge-bin ];
};

# Needed for games
hardware.graphics = {
  enable = true;
  enable32Bit = true;
};

# Gamemode: better performance for fullscreen games
programs.gamemode.enable = true;

After a rebuild, launch steam from the app menu. Proton / native games / Workshop all work.

hardware.graphics.enable32Bit = true; is required for 32-bit GL — many games still ship 32-bit binaries.

2. programs.nix-ld — fake FHS dynamic linker#

For arbitrary prebuilt binaries (VSCode extensions with native code, pip wheels with native deps, pnpm/npm native modules, rustup-installed toolchains, vendor binaries):

programs.nix-ld = {
  enable = true;
  libraries = with pkgs; [
    stdenv.cc.cc.lib
    zlib zstd
    openssl
    curl
    glib
    libxkbcommon
    icu
    fuse3
    xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr xorg.libXxf86vm
    libGL
  ];
};

This plants a shim at /lib64/ld-linux-x86-64.so.2 that points to a curated library set. Most random binaries now start without you touching them.

3. appimage-run — one-command AppImage launcher#

programs.appimage = {
  enable = true;
  binfmt = true;     # run AppImages just by double-clicking / ./foo.AppImage
};

With binfmt = true, AppImages behave like on Debian. Without it:

appimage-run ./Cursor.AppImage

3b. Declaratively install a remote AppImage#

To turn an AppImage URL into a real, installed, PATH-available binary that ships with your config — use appimageTools.wrapType2 together with fetchurl. The result is pinned by hash, reproducible on any machine, and survives reinstalls.

{ pkgs, ... }:
let
  cursor = pkgs.appimageTools.wrapType2 {
    pname   = "cursor";
    version = "0.42.0";
    src = pkgs.fetchurl {
      url  = "https://downloader.cursor.sh/linux/appImage/x64";
      hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
    };
  };
in {
  environment.systemPackages = [ cursor ];
}

Getting the hash the first time. Set hash = lib.fakeSha256; (or any obviously-wrong value), then nixos-rebuild build. The build fails with a message like:

got:    sha256-rq4GOVm3...=

Copy that hash into hash = ... and rebuild — it succeeds.

Adding a .desktop entry, icon, and proper app menu integration. The single-line wrapType2 call gets you a working binary but no application-menu entry. For that, extract the AppImage first and hand-install the metadata:

let
  pname = "obsidian";
  version = "1.7.7";

  src = pkgs.fetchurl {
    url = "https://github.com/obsidianmd/obsidian-releases/releases/download/v${version}/Obsidian-${version}.AppImage";
    hash = "sha256-REPLACE_ME=";
  };

  appimageContents = pkgs.appimageTools.extractType2 {
    inherit pname version src;
  };
in
pkgs.appimageTools.wrapType2 {
  inherit pname version src;

  extraInstallCommands = ''
    install -m 444 -D ${appimageContents}/${pname}.desktop \
      $out/share/applications/${pname}.desktop
    install -m 444 -D ${appimageContents}/${pname}.png \
      $out/share/icons/hicolor/512x512/apps/${pname}.png
    substituteInPlace $out/share/applications/${pname}.desktop \
      --replace 'Exec=AppRun' 'Exec=${pname}'
  '';
}

That gives you:

  • A binary on PATH (e.g. obsidian).

  • A menu entry so it shows up in KRunner / the app launcher.

  • An icon rendered everywhere apps are rendered.

Keeping a private collection. Put each AppImage in its own file under ./packages/<name>.nix, import them from configuration.nix:

environment.systemPackages = [
  (pkgs.callPackage ./packages/cursor.nix {})
  (pkgs.callPackage ./packages/obsidian.nix {})
  (pkgs.callPackage ./packages/balena-etcher.nix {})
];

Auto-updating the version. AppImage URLs from GitHub releases can be bumped by running nix-update:

nix-shell -p nix-update --run 'nix-update --file ./packages/obsidian.nix obsidian'

…or, for less-scripted workflows, bump version manually, set hash = lib.fakeSha256;, rebuild to learn the new hash, paste.

wrapType1 vs wrapType2. Modern AppImages are “type 2” (the overwhelmingly common format since 2017). Use wrapType2. wrapType1 is only needed for pre-2017 AppImages (rare).

On NixOS, a .AppImage file by itself cannot run — the dynamic linker paths are wrong. appimage-run or appimageTools.wrapType2 both fix that; the difference is that wrapType2 gives you a permanent, config-managed package, while appimage-run is for one-off launches.

4. steam-run — run any binary inside Steam’s FHS#

Steam’s runtime includes hundreds of common libs, so steam-run is a swiss-army knife for running third-party binaries:

steam-run ./some-vendor-binary
steam-run bash               # drop into an FHS shell

Install it alongside Steam or standalone:

environment.systemPackages = with pkgs; [ steam-run ];

5. buildFHSEnv — bespoke FHS environment#

When the above aren’t enough, wrap your own FHS shell with exactly the libs you need:

(pkgs.buildFHSEnv {
  name = "my-fhs";
  targetPkgs = pkgs: with pkgs; [
    gcc glibc zlib openssl curl libxml2
    python312 nodejs
  ];
  runScript = "bash";
})

Drop that into environment.systemPackages; my-fhs becomes a command that opens a Debian-like shell.

6. Packaging a binary yourself — autoPatchelfHook#

For something you want as a proper system package:

pkgs.stdenv.mkDerivation rec {
  pname = "vendor-tool";
  version = "1.2.3";

  src = pkgs.fetchurl {
    url = "https://vendor.example/${pname}-${version}-linux-x64.tar.gz";
    hash = pkgs.lib.fakeHash;      # replace after first build
  };

  nativeBuildInputs = [ pkgs.autoPatchelfHook ];

  # Libraries the vendor binary expects at runtime. autoPatchelfHook rewrites
  # every ELF's rpath so that they resolve into the Nix store.
  buildInputs = with pkgs; [
    stdenv.cc.cc.lib
    zlib
    openssl
  ];

  # The tarball extracts into the current directory. Layout is assumed to be
  # ./bin/vendor-tool, ./lib/*, ./share/*. Drop those straight into $out.
  installPhase = ''
    runHook preInstall
    mkdir -p $out
    cp -r ./. $out/
    runHook postInstall
  '';

  meta.mainProgram = "vendor-tool";   # enables `nix run .#vendor-tool`
}

autoPatchelfHook walks $out after install, finds every ELF, and rewrites its rpath to point at libs from buildInputs (in the Nix store). The binary then finds its libs at runtime without any FHS layer.

Two common pitfalls fixed in the snippet above vs older tutorials:

  • Don’t self-symlink $out/bin/vendor-tool to itself. Older recipes did ln -s $out/bin/foo $out/bin/foo which just creates a broken symlink. If $out/bin/vendor-tool already exists because the tarball had a bin/ directory, cp -r ./. $out/ places it correctly and you’re done. If the binary lives at the tarball root, use install -Dm755 vendor-tool $out/bin/vendor-tool — never a self-symlink.

  • Use hash, not sha256. The newer hash attribute accepts any hash algorithm with its SRI prefix (sha256-..., sha512-...). On first build, set hash = pkgs.lib.fakeHash;, read the expected hash from the build failure, paste it in. Old tutorials use sha256 = lib.fakeSha256 — still works but deprecated.

For tarballs that ship a simple bin/foo + lib/libfoo.so layout, the recipe above is all you need. For vendor installers that expect a specific directory structure (opt/company/tool/bin/foo), mimic it inside $out and add wrappers:

installPhase = ''
  runHook preInstall
  mkdir -p $out/opt/vendor-tool $out/bin
  cp -r ./. $out/opt/vendor-tool/
  makeWrapper $out/opt/vendor-tool/bin/vendor-tool $out/bin/vendor-tool
  runHook postInstall
'';

nativeBuildInputs = [ pkgs.autoPatchelfHook pkgs.makeWrapper ];

Flatpak as escape hatch#

Flatpak coexists with Nix and is the right answer for a few categories: proprietary apps with complex sandboxing needs, desktop apps only packaged for Flathub, things that break nix-ld.

services.flatpak.enable = true;
xdg.portal = {
  enable = true;
  extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
};

Add Flathub (one-time, user-level):

flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install flathub com.spotify.Client

Overlays (bumping or patching a single package)#

Overlays let you override one package without forking nixpkgs. Common case: you want a newer version of a tool than the current channel ships.

nixpkgs.overlays = [
  (final: prev: {
    # use a specific GitHub commit
    my-tool = prev.my-tool.overrideAttrs (old: {
      version = "1.2.3-custom";
      src = prev.fetchFromGitHub {
        owner = "example";
        repo = "my-tool";
        rev = "v1.2.3";
        sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
      };
    });

    # pull from unstable for one package
    firefox = (import (builtins.fetchTarball {
      url = "https://github.com/NixOS/nixpkgs/archive/nixos-unstable.tar.gz";
    }) { system = prev.system; config.allowUnfree = true; }).firefox;
  })
];

For flakes, add inputs.nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable"; and reference inputs.nixpkgs-unstable.legacyPackages.${system}.firefox directly — cleaner than the tarball trick.

Next#

  • nix-language.md — the with pkgs;, lib.*, attribute-set syntax used throughout this page.

  • dev-tools.md — the same nix-ld / buildFHSEnv story applied to language toolchains.

  • snippets.md — copy-paste recipes for common services.

  • tricks.md — VM, live USB, nixos-anywhere, rollback.