Nix in WSL: Reproducible Development Shells Inside a Linux Distro
Use the Nix package manager in WSL for project-scoped development shells, pinned inputs, and reproducible tools without confusing Nix with NixOS.
Nix can provide project-scoped development environments inside a WSL distribution without replacing the distribution itself. This distinction matters: installing the Nix package manager in Ubuntu or Debian under WSL does not install NixOS or take ownership of the host’s init system. It adds a package store and, depending on installation mode, a daemon that evaluates and builds Nix expressions.
The official Nix download page now includes WSL-specific guidance. It documents a multi-user installation when WSL systemd is enabled and a single-user alternative. Choose deliberately based on the project and the privileges available. Keep the Nix store and development checkout on the Linux filesystem. The store can grow substantially, and a Windows-mounted path introduces a different filesystem boundary than Nix expects for Linux builds.
Choose an installation mode that matches WSL
Before installation, record the WSL version, distribution, Linux architecture, and whether systemd is enabled. The Nix documentation’s WSL installation instructions have requirements for the multi-user daemon path. If systemd is not enabled, do not copy the multi-user command and assume the daemon will start. Review the official single-user guidance and its limitations, or enable systemd according to Microsoft’s WSL procedure before selecting the multi-user path.
The installer changes system state and creates /nix plus supporting configuration. Read the current Nix installation documentation before running it; do not pipe an installer script into a root shell without reviewing the procedure. Decide where the store’s VHDX growth will be accounted for and how Nix will be uninstalled or repaired. Keep installer logs and the exact Nix version for the workstation record.
After installation, confirm the executable and daemon state from Linux. In a multi-user setup, verify the service through systemd and inspect the Nix store path. In a single-user setup, confirm the user profile and environment initialization. Avoid appending duplicate Nix profile snippets to shell startup files; inspect .profile, .bashrc, and other startup paths to understand where PATH is set.
Build a project-scoped shell
Nix development shells let a project describe tools and libraries that should be available while working in that checkout. If a repository uses a flake-based shell, nix develop enters that environment; the project’s Nix configuration and lockfile determine the inputs. Nix command feature availability can depend on the installed version and configuration, so follow the project’s documented requirements rather than assuming every host has identical experimental-feature settings.
The shell is not a magic container. Processes still run in the WSL distribution and see its kernel, filesystem, network, and mounted paths. If a build needs a compiler, SDK, or library, declare it in the project’s environment instead of relying on a global package that happens to be installed on one developer’s machine. If the build invokes Windows executables through interop, that remains an undeclared host dependency unless the project documents it.
Use a clean terminal to compare the shell with the base distribution. Record command -v and version output for important tools before and after entering nix develop. Run the project’s formatter, tests, or build from the Nix shell and confirm the output directory remains within the checkout or a documented Linux path. For a reproducibility check, use a fresh WSL user or clean store cache only when the resource cost is understood; a warm store can hide downloads and undeclared state.
Treat lockfiles and channels as dependency contracts
Nix inputs determine package versions and build derivations. A lockfile records resolved revisions for flake inputs; review its changes just as you would a language dependency lockfile. Updating a lockfile can change many packages at once, so compare the input diff and rerun the relevant project checks. Do not regenerate it automatically in CI unless the project has explicitly chosen floating inputs.
Channels, flakes, and pinned tarball inputs are different dependency selection mechanisms. Avoid mixing them casually in the same shell. A shell that uses an unpinned moving channel can resolve a different compiler next month even though flake.nix did not change. Document the update cadence and how changes are promoted into the repository.
Use project shells to isolate tool versions, not to hide system integration requirements. A package may depend on Linux capabilities, GPU access, a mounted device, or a daemon supplied by the WSL distribution. Verify those prerequisites separately. A Nix shell can select a CUDA toolkit or compiler package, but it cannot by itself grant a workload a working WSL GPU driver interface; use the existing WSL GPU integration guidance for that boundary.
Store placement, permissions, and garbage collection
The Nix store is shared package state. Do not place it inside a short-lived project directory or a Windows-mounted drive as a shortcut. Keep project source in the repo and the Nix store in the configured Linux location. Understand which user owns the store and which daemon performs builds; multi-user and single-user modes have different ownership and isolation behavior.
Nix garbage collection can reclaim unreachable store paths, but do not run broad collection commands during a build or before confirming what profiles and generations are retained. Use the manual’s supported garbage collection procedure and inspect profile roots first. A package that appears unused may be pinned by a shell, profile, or another user’s reference. Nix’s store size and WSL’s virtual disk allocation are also different: deleting store paths frees filesystem blocks but may not immediately shrink the VHDX file on Windows.
If a project uses binary caches, understand which cache URLs and trust configuration are in effect. A cache hit means Nix fetched a substitutable build output, not that the project source or cache policy is automatically trustworthy. Follow organizational policy for allowed substituters and trusted public keys. Do not paste private cache credentials into a flake or commit them to the repository.
Project shells are not a replacement for operating-system services. If a package expects a daemon, mounted device, kernel feature, or system library outside the declared shell, validate that dependency in the base WSL distro. Keep service state such as databases and caches out of the Nix store; the store is for package derivations, not mutable application data. A successful nix develop entry proves the environment evaluated, not that every build command or runtime service works.
For team adoption, define how Nix is updated and how a change to flake.lock is reviewed. A developer should be able to enter the shell from the repository root and obtain the intended tools without manually sourcing a second environment. If a non-flake project uses a different Nix workflow, document that interface rather than adding a flake solely to make the command in this article fit.
When a locked input changes, review the diff for every updated revision and rerun the project checks that rely on those packages. A successful evaluation only proves that Nix could construct the environment expression; it does not prove that a compiler works, a test passes, or an external service is reachable. Preserve the command and lockfile revision used for a release build so another developer can reproduce the same input graph. If a development shell depends on mutable files outside the repository, document those separately rather than implying the lockfile captures them.
Troubleshoot WSL-specific failures
If nix develop cannot find a command, inspect the shell environment and the devShell definition, then verify that the project lockfile selected the expected input. If daemon commands fail, check the selected installation mode and systemd status. If a build cannot create files, inspect ownership of /nix, the checkout, and output paths rather than recursively changing permissions.
If builds are unexpectedly slow, separate evaluation, download, substitution, local compilation, filesystem I/O, and CPU pressure. Compare a Linux filesystem checkout with /mnt/c only under controlled conditions. If the VHDX becomes large, identify store paths and project outputs before garbage collection. Do not reset the entire Nix store to diagnose a single failed derivation.
Acceptance criteria
Accept a WSL Nix setup when the distribution and Nix versions are recorded, installation mode matches systemd availability, store ownership is understood, a project shell provides the declared tools, its lock inputs are reviewed, and a representative test or build succeeds from a fresh shell. Document which host capabilities remain external to Nix.
Nix in WSL is a package and development-environment tool within a Linux distribution. It is not NixOS, an isolated VM, a security boundary, or a replacement for validating builds on the target CI platform.
Related:
- .wslconfig vs. wsl.conf: Two Configuration Scopes That Should Not Be Mixed
- WSL systemDistro: The Narrow Contract for a Custom System Image
Sources: