Skip to content
WSLDeep Dive Published Updated 7 min readViews unavailable

Node.js in WSL: Keep npm, Native Modules, and the Workspace on Linux

Choose one Node.js runtime per WSL project, isolate npm and native modules from Windows, and validate package-manager and filesystem boundaries.

Node.js tooling can run in Windows or inside WSL, but those are separate runtime environments. A Windows node.exe, Windows npm global prefix, and Windows-built native addon do not become Linux components when a shell is opened in Ubuntu. Conversely, a Linux Node process should not install dependencies under a Windows node_modules tree and expect platform-specific modules to load.

For software that deploys to Linux or uses Linux-specific tooling, Microsoft recommends installing Node.js in WSL and keeping the project files in the Linux filesystem. That gives npm, native modules, file permissions, symlinks, and build scripts a consistent platform owner.

Decide which operating system owns the project

Choose the runtime based on the target and tooling rather than on which terminal is most familiar:

  • Use Windows Node.js when Windows-native applications or a Windows deployment target own the workflow.
  • Use Linux Node.js inside WSL when the app targets Linux, the build depends on Linux utilities, or CI runs in a Linux environment.
  • Do not combine Windows Node and WSL Node in one dependency installation directory.

If both runtimes are needed on one machine, make their executable paths and package directories intentionally separate. Check the decision from PowerShell with Get-Command node and from WSL with command -v node. The command names are the same, but they can point to different binaries and different npm installations.

Install a Linux runtime using a maintained version manager

Microsoft’s WSL Node.js guidance describes using a Linux version manager such as nvm so a developer can install and switch Node versions within the distribution. Follow the current official installation instructions for the tool and verify the release channel your project supports. Avoid copying a long-lived installation script into a profile without reviewing how it is maintained.

After installation, start a new shell and check:

command -v node
command -v npm
node --version
npm --version
node -p 'process.execPath'
npm config get prefix

The resolved paths should be inside the intended Linux environment, normally under the distro filesystem or the version manager’s Linux-owned data directory. If node resolves under /mnt/c, inspect the WSL interop path and shell initialization before building.

Do not assume that the latest Node release is compatible with the application. Use the release range declared by the repository, CI configuration, or deployment image. A .nvmrc or engines field may express part of that contract, but confirm how the chosen manager and package manager interpret it.

Keep the project and dependencies together

Place a Linux-owned project under the distro filesystem, for example ~/src/web-api, and run its Linux package manager there. This is particularly important for large dependency trees and file-watch-heavy build systems. WSL supports access to Windows files through mounted paths, but Microsoft cautions that Linux workloads can perform differently when the project is stored under /mnt/c.

If Windows editors need to work on the project, use a WSL-aware remote development connection that runs project tooling inside the distribution. Avoid opening the same checkout simultaneously with independent Windows and Linux installers that both mutate node_modules, generated output, or lockfiles.

Treat node_modules as platform-specific build output

Many npm packages are pure JavaScript, but packages can also contain native addons or install scripts that select or compile code for a specific OS, CPU architecture, and Node ABI. A successful Windows npm install does not prove that the Linux dependency graph is usable in WSL. Copying a node_modules directory across the boundary can preserve the wrong binaries and symlinks.

Treat node_modules as disposable output. Keep it out of version control, then install from the committed lockfile on the owning platform. For npm projects, npm ci is the reproducible clean-install command when a compatible package-lock.json is committed. It removes the existing dependency directory and fails if the lockfile and package manifest disagree; it is not a lockfile-generation command.

cd ~/src/web-api
node --version
npm --version
npm ci
npm run build
npm test

Do not run npm ci in a directory that contains unique manually edited files under node_modules. Generated dependency trees should be reconstructible; application source and configuration belong in the repository.

Distinguish runtime identity from npm configuration

Node resolves executable modules using its runtime paths; npm also has configuration sources such as project, user, and global config. Print the effective configuration before diagnosing a package that goes to an unexpected directory:

node -p 'process.platform + " " + process.arch'
npm config get prefix
npm config get cache
npm config list
printf '%s\n' "$PATH"

Review config output before sharing it because a registry URL or environment may contain internal names or credentials. A global package prefix that points into Windows can make global commands work inconsistently. Prefer project-local scripts and a Linux-owned prefix unless there is a specific administrative reason for a global installation.

The npm cache is not a source-of-truth dependency store. It can be revalidated or rebuilt. Keep package integrity and resolved versions anchored in the lockfile and package registry policy rather than in a developer’s cache.

When a repository declares a package manager or runtime range, record how local shells and CI select it. A project may require a particular major line even while the machine has another one globally installed. A clean install should fail visibly when the runtime is incompatible, rather than silently switching a team member’s global setup. Put project-specific commands in package scripts and invoke them with the selected Linux Node executable.

Lockfile changes need review like source changes. Compare the lock before and after dependency updates, confirm that the configured registry is expected, and use the same package manager family that generated the lock. npm, pnpm, and Yarn may have different lock formats and install semantics. Avoid running multiple package managers in the same checkout unless the repository documents why it keeps multiple locks.

Account for native modules and architecture

Native Node modules may be distributed as prebuilt binaries or built locally. A module can fail because the Linux system lacks a compiler, Python build helper, development headers, a compatible libc, or the package’s target architecture. Diagnose the error from the Linux install log. Do not copy a .node binary compiled on Windows into the Linux tree or treat an x64 package as valid on ARM64.

When the project has an optional native dependency, test both the clean install and the relevant runtime path. If the dependency uses Node-API, ABI stability may improve compatibility across some Node releases, but it does not make an operating-system-specific binary portable. Use the package’s own compatibility policy.

For reproducible release builds, record the Node release line, Linux distro, architecture, system build dependencies, package manager, and lockfile revision. A dependency lock cannot pin the compiler or operating system ABI by itself.

Debug scripts that behave differently under WSL

Lifecycle scripts execute in an environment chosen by the package manager. A script that calls bash, sed, or rm may work in Linux and fail in Windows; a script that invokes a Windows path may behave differently under Linux. Inspect the process environment and package script rather than adding platform conditionals everywhere.

For each build, record the actual executable:

type -a node npm
node -p 'process.execPath'
pwd
git status --short

If a watcher misses changes, first establish whether the editor and watcher observe the same filesystem and process environment. A watch service cannot be fixed by reinstalling npm when the source tree is being edited from a different OS path.

The engines field in a package manifest can communicate a runtime range, but enforcement depends on the package manager’s configuration and behavior. Treat it as metadata to compare, not as proof that the current runtime is rejected. CI should assert the intended runtime explicitly and print the selected version before installation. When a monorepo has multiple packages, check the root and package-specific constraints rather than assuming one root manifest governs every command.

Acceptance checks

A repeatable WSL Node environment passes when a fresh Linux checkout can select the repository-supported Node release, install exactly from the committed lockfile, run build and test scripts, and load native dependencies without Windows paths. Verify that process.platform reports Linux, process.arch matches the guest, and process.execPath points to the selected Linux runtime.

Test both a clean install and a second no-change run of CI-equivalent commands. Capture the package-manager versions and distro identity with the results. If Windows Node is installed too, verify separately that opening the Windows project does not mutate the Linux dependency tree and vice versa.

Related:

Sources:

Comments