# How to use uv on NixOS


[uv](https://pydevtools.com/handbook/reference/uv.md) ships as a single static binary that drops onto NixOS without a build step. The hard part is what happens next: uv's [managed Python interpreters](https://pydevtools.com/handbook/how-to/how-to-install-python-with-uv.md) are precompiled for a generic Linux that NixOS doesn't pretend to be, so `uv sync` and `uv run` fail with `Could not start dynamically linked executable` until you bridge the gap.

This guide walks through the two patterns NixOS users reach for: enabling `nix-ld` system-wide so uv-downloaded Python binaries find their dynamic linker and libraries, and pinning uv inside a `flake.nix` dev shell that sets per-project linker and library variables. Pick the system-wide pattern when you want uv to work everywhere, and add the flake pattern when a repository should carry its own uv version and library list.

## Why uv-managed Python fails on NixOS

NixOS does not follow the [Filesystem Hierarchy Standard](https://wiki.nixos.org/wiki/Filesystem_Hierarchy_Standard). Shared libraries live under `/nix/store/<hash>-<name>/lib/`, not `/usr/lib/`, and the system has no `ld-linux-x86-64.so.2` at the path that prebuilt binaries hardcode. Native Nix packages get patched at build time so their interpreter and library paths point inside `/nix/store`. Anything you download from the open internet does not.

uv's managed Python builds are downloaded at runtime from [python-build-standalone](https://github.com/astral-sh/python-build-standalone), so they hit this wall the first time uv tries to use one:

```console
$ uv sync
error: Querying Python at `~/.local/share/uv/python/cpython-3.13.../bin/python3.13` failed
  Caused by: Could not start dynamically linked executable
```

The fix has two parts: teach NixOS how to start unpatched binaries, then expose the libraries those binaries need. Pattern 1 handles both system-wide. Pattern 2 pins the linker and library variables per project, but on NixOS it still requires the `nix-ld` shim to exist on the host; the flake can provide `NIX_LD` and `NIX_LD_LIBRARY_PATH`, but it cannot create `/lib64/ld-linux-x86-64.so.2` by itself.

## Pattern 1: Install uv system-wide and enable nix-ld

If you administer the NixOS machine and want uv to work in every shell, this is the shortest path. [`nix-ld`](https://github.com/nix-community/nix-ld) installs a shim at `/lib64/ld-linux-x86-64.so.2` that reads `NIX_LD` and `NIX_LD_LIBRARY_PATH` to redirect any unpatched binary to the real linker and the right libraries.

Add the following to `/etc/nixos/configuration.nix`:

```nix {filename="/etc/nixos/configuration.nix"}
{ pkgs, ... }: {
  environment.systemPackages = with pkgs; [
    uv
  ];

  # Tell uv to install user-level binaries onto $PATH.
  environment.localBinInPath = true;

  # Let unpatched binaries (uv-managed Python, pip wheels with C deps)
  # find a dynamic linker and the shared libraries they expect.
  programs.nix-ld = {
    enable = true;
    libraries = with pkgs; [
      stdenv.cc.cc.lib   # libstdc++.so.6 for most wheels
      zlib               # CPython, many compiled extensions
      openssl
      libffi
      glibc
    ];
  };
}
```

Apply the change:

```bash
sudo nixos-rebuild switch
```

Open a new shell and verify uv can fetch and run a managed interpreter:

```bash
uv python install 3.13
uv run --python 3.13 python -c "import ssl, zlib; print(ssl.OPENSSL_VERSION)"
```

If the second command prints an OpenSSL version instead of a linker error, nix-ld is doing its job. Add packages to `programs.nix-ld.libraries` whenever a new wheel imports a C library that's missing; the [NixOS Wiki page on nix-ld](https://wiki.nixos.org/wiki/Nix-ld) lists the ones most projects need.

> [!NOTE]
> nix-ld is a NixOS module. On non-NixOS systems where Nix is installed alongside a regular distribution, use Pattern 2 instead. The host's standard dynamic linker handles the rest.

## Pattern 2: Pin uv in a flake.nix dev shell

When the repository has to work for teammates who don't run NixOS, declare uv and its dependencies inside a `flake.nix` dev shell. Anyone with Nix and flakes enabled gets the same uv version and the same library paths.

> [!IMPORTANT]
> On NixOS, this pattern still requires `programs.nix-ld.enable = true;` from Pattern 1. The flake supplies the per-project `NIX_LD` and `NIX_LD_LIBRARY_PATH` values, but it cannot create the `/lib64/ld-linux-x86-64.so.2` shim those values feed. On non-NixOS Linux, uv's managed Python runs against the host's standard linker, so these variables are inert and harmless.

The flake deliberately leaves `LD_LIBRARY_PATH` unset. It applies to every binary in the shell, so it overrides the `RPATH` of correctly-built Nix programs and feeds them the wrong libraries, and on non-NixOS Linux it forces the host linker to load a mismatched Nix glibc that crashes both managed and system Python (a segfault or a `Could not start dynamically linked executable` error, depending on the version gap). nix-ld reads `NIX_LD_LIBRARY_PATH` instead, which only its shim consults, so uv's managed Python gets its libraries without disturbing anything else.

Create `flake.nix` at the project root:

```nix {filename="flake.nix"}
{
  description = "uv-managed Python project";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};
        libs = with pkgs; [ stdenv.cc.cc.lib zlib openssl libffi glibc ];
        libPath = pkgs.lib.makeLibraryPath libs;
      in {
        devShells.default = pkgs.mkShell {
          packages = [ pkgs.uv ];
          env.NIX_LD = pkgs.stdenv.cc.bintools.dynamicLinker;
          env.NIX_LD_LIBRARY_PATH = libPath;
          # Don't set LD_LIBRARY_PATH here: on non-NixOS Linux it forces the host's
          # dynamic linker to load Nix's glibc and crashes Python. nix-ld reads
          # NIX_LD_LIBRARY_PATH instead, so the shim gets the libraries either way.
        };
      });
}
```

Enter the shell and use uv normally:

```bash
nix develop
uv init demo
cd demo
uv add requests
uv run python -c "import requests; print(requests.__version__)"
```

`nix develop` drops you into a shell with `uv` on `PATH`, `NIX_LD` pointing at the Nix dynamic linker, and `NIX_LD_LIBRARY_PATH` listing the project's libraries. uv downloads its managed Python and installs `requests` into `.venv`; the import then succeeds.

`nix develop` writes a `flake.lock` on its first run. Commit it with `git add flake.nix flake.lock` so every checkout resolves the same `nixpkgs` commit, and therefore the same uv version and libraries, instead of whatever the `nixos-26.05` channel has moved to since. Run `nix flake update` when you want newer packages.

Pair the flake with [direnv](https://pydevtools.com/handbook/reference/direnv.md) so the dev shell loads automatically when you `cd` into the project:

```bash {filename=".envrc"}
use flake
```

Run `direnv allow` once. After that, every `cd` into the directory enters the dev shell; every `cd` out leaves it.

> [!TIP]
> Add libraries to the `libs` list when a new dependency fails to import. Scientific stacks usually need `pkgs.gcc-unwrapped.lib`; GPU wheels also need `pkgs.cudaPackages.cudatoolkit`.

## When to reach for uv2nix instead

The two patterns above let uv install and run Python packages from PyPI. Those packages stay impure: they come from the network into a venv, outside `/nix/store`. For Nix users who want every dependency built from source through the Nix store, [`uv2nix`](https://pyproject-nix.github.io/uv2nix/) parses `uv.lock` and emits Nix expressions that reproduce the resolved environment as Nix derivations.

Reach for uv2nix when you need bit-identical builds across machines or when you're packaging a Python application for Nix consumers. Skip it for day-to-day development; the patterns above let uv's lockfile stay the source of truth.

## Why a working dev shell suddenly breaks

A flake dev shell that ran managed Python and later fails on `nix develop` has drifted its glibc. The failure shows up as `Could not start dynamically linked executable`, a segfault, or a `symbol lookup error: ... version GLIBC_PRIVATE`. All three are the same problem: the loader that starts the interpreter and the `libc.so.6` it loads came from different glibc builds, and glibc requires them to match.

A flake that sets only `LD_LIBRARY_PATH` is exposed to this. It pins the libraries but takes the loader from the system, so a `nix flake update`, a system rebuild, or a `nix-collect-garbage` that evicts and re-fetches a store path moves one glibc out from under the other. Setting `NIX_LD` and `NIX_LD_LIBRARY_PATH` from the same `pkgs`, as the Pattern 2 flake does, keeps the loader and the libraries on one glibc, so they cannot drift apart. If a shell already broke, adopt the Pattern 2 flake and clear any `LD_LIBRARY_PATH` still exported by a shell profile or `.envrc`, then re-enter with `nix develop`.

## Fix common errors

`Could not start dynamically linked executable` after `uv python install` succeeded: the binary is on disk but the dynamic linker isn't in place. On NixOS, enable `programs.nix-ld.enable = true;` (Pattern 1). Inside a flake dev shell, confirm `NIX_LD` points at a Nix dynamic linker and `NIX_LD_LIBRARY_PATH` lists `/nix/store` libraries:

```bash
echo "$NIX_LD"
echo "$NIX_LD_LIBRARY_PATH" | tr ':' '\n'
```

`ImportError: libstdc++.so.6: cannot open shared object file`: a wheel needs the C++ runtime. Add `pkgs.stdenv.cc.cc.lib` to `programs.nix-ld.libraries` (Pattern 1) or to the `libs` list in `flake.nix` (Pattern 2), then rebuild or re-enter the shell.

uv tries to download Python on every `uv sync`: set [`UV_PYTHON_DOWNLOADS=never`](https://docs.astral.sh/uv/reference/settings/#python-downloads) and install Python with `uv python install <version>` once. uv will reuse the cached interpreter at `~/.local/share/uv/python/`.

uv binaries land somewhere your shell can't find them: if `uv tool install` succeeds but the tool is `command not found`, set `environment.localBinInPath = true;` in `/etc/nixos/configuration.nix` so `~/.local/bin` is on `PATH`. Outside NixOS, add it to the shell profile manually.

## Learn More

- [uv: A Complete Guide](https://pydevtools.com/handbook/explanation/uv-complete-guide.md) covers what uv does, how fast it is, the core workflows, and recent releases.
- [Python quickstart using uv (NixOS Wiki)](https://wiki.nixos.org/wiki/Python_quickstart_using_uv) lists every NixOS option uv responds to, with the exact configuration keys.
- [`nix-community/nix-ld`](https://github.com/nix-community/nix-ld) explains how the linker shim works and what `NIX_LD_LIBRARY_PATH` overrides at runtime.
- [`pyproject-nix/uv2nix`](https://pyproject-nix.github.io/uv2nix/) shows the templates and flake structure for building uv projects fully through Nix.
- [How to install uv on Linux](https://pydevtools.com/handbook/how-to/how-to-install-uv-on-linux.md) covers the standalone installer for non-Nix Linux distributions.
- [How to customize uv's virtual environment location](https://pydevtools.com/handbook/how-to/how-to-customize-uvs-virtual-environment-location.md) is useful when a Nix dev shell mounts the project from a read-only path.
