How to use uv on NixOS
uv 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 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. 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, so they hit this wall the first time uv tries to use one:
$ 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 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:
{ 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:
sudo nixos-rebuild switchOpen a new shell and verify uv can fetch and run a managed interpreter:
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 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:
{
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:
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 so the dev shell loads automatically when you cd into the project:
use flakeRun 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 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:
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 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 covers what uv does, how fast it is, the core workflows, and recent releases.
- Python quickstart using uv (NixOS Wiki) lists every NixOS option uv responds to, with the exact configuration keys.
nix-community/nix-ldexplains how the linker shim works and whatNIX_LD_LIBRARY_PATHoverrides at runtime.pyproject-nix/uv2nixshows the templates and flake structure for building uv projects fully through Nix.- How to install uv on Linux covers the standalone installer for non-Nix Linux distributions.
- How to customize uv’s virtual environment location is useful when a Nix dev shell mounts the project from a read-only path.