You want a dev box that behaves like production Linux, but your laptop came with Windows, a corporate image, and a “developer experience” that’s mostly registry keys and regret.
WSL is the compromise that actually works—if you stop treating it like “Linux but in a folder” and start treating it like a real system with boundaries.
This is the playbook I use when I need Node, Python, and Go installed cleanly, versioned sanely, and reproducible across a team—without smearing dependencies across Windows,
breaking PATH, or turning file I/O into a performance art piece.
The rules: keep Windows clean, keep WSL honest
A clean dev environment is mostly about refusing to “just install it real quick.” You can absolutely install Node on Windows, Python on Windows, Go on Windows,
then install them again in WSL, then wonder why your tooling randomly uses the wrong binary. That’s not a rite of passage. That’s a preventable incident.
My opinionated default:
- Install language runtimes inside WSL, not on Windows. Use Windows only for editors/terminals and optional GUI tools.
- Use version managers (nvm, pyenv) unless you have a controlled monorepo with pinned toolchains baked into containers.
- Keep source code and dependency caches in the Linux filesystem (inside the WSL distro), not under
/mnt/c. - Keep Windows PATH out of WSL PATH unless you have a specific reason. Interop is a scalpel, not a diet.
- Automate setup with a bootstrap script and dotfiles. If your environment can’t be rebuilt, it’s not an environment; it’s a pet.
Exactly one quote, because it’s worth the ink. Gene Kim’s paraphrased idea is: reliability comes from systems and feedback loops, not heroics
.
WSL setup is the same: boring defaults beat clever hacks.
Joke #1: The quickest way to learn about PATH precedence is to break PATH precedence.
The second quickest way is to read the rest of this article.
Facts and history you can weaponize
A few short facts that explain why WSL behaves the way it does—and why the “easy” install path often becomes the expensive one.
- WSL 1 and WSL 2 are different beasts. WSL 2 uses a real Linux kernel in a lightweight VM; WSL 1 translated Linux syscalls. Performance and semantics differ.
- WSL 2 networking is NAT’d by default. Localhost usually works, but some corporate proxies, VPNs, and port exposure assumptions will betray you.
- Cross-filesystem access is asymmetric. Linux filesystem access from WSL is fast; accessing Windows files under
/mnt/cis slower, especially for many small files (hello,node_modules). - Node tooling got more complex over time. npm became a platform; Yarn and pnpm fought for mindshare; Corepack showed up to standardize package manager versions.
- Python packaging is still a warzone. Wheels vs source builds, system libraries, and virtual environments create failure modes that look like “Python is broken” but are really “your build inputs are inconsistent.”
- Go deliberately reduced environment complexity. Modules (Go 1.11+) moved dependency management away from GOPATH-centric workflows, but GOPATH still matters for caches and older tooling.
- Ubuntu’s packages trade freshness for stability. Apt packages can lag language releases. Version managers exist because “latest” and “secure” are not synonyms.
- Corporate Windows images are opinionated. They ship with Python launchers, old Git, and security agents that hook file I/O. Mixing runtimes across Windows and WSL makes debugging twice as fun and half as useful.
Foundation: choose your distro, set boundaries, and verify WSL health
Pick one WSL distro per “persona.” For most people that’s one Ubuntu distro for day-to-day work. Multiple distros are fine when you need isolation
(e.g., one for legacy Python 2 archaeology, one for modern tooling), but don’t create ten distros because you can’t decide between shells.
WSL version and distro sanity
Before installing any language toolchains, confirm you’re on WSL 2 and that your distro is healthy. If your base is shaky, every install step turns into folklore.
Boundary #1: stop auto-inheriting Windows PATH unless you mean it
The sneakiest breakage happens when WSL “helpfully” includes Windows paths. Suddenly your python inside WSL points at a Windows executable,
which then tries to read Linux paths and fails in creative ways.
Disable that by editing /etc/wsl.conf inside the distro and then restarting WSL.
Boundary #2: treat the WSL filesystem as your dev disk
Put repos under ~ (or another Linux path), not under /mnt/c/Users/.... The latter will burn you on performance and tooling edge cases.
If you must operate on Windows files (compliance, shared drives), isolate that workflow and accept it will be slower.
Filesystem placement: where your code lives decides your speed
Your toolchains aren’t slow; your storage path is slow. Node and Python are especially sensitive because they create and read thousands of small files.
If those live under /mnt/c, every filesystem operation takes the long way through interop.
The rule is simple:
- Code + dependencies inside WSL’s ext4 filesystem (typically under
/home) for speed. - Windows-mounted paths for occasional file exchange, not for builds.
When someone says “WSL is slow,” I ask one question: “Where is the repo located?” Most of the time, that’s the whole mystery.
Node.js in WSL (nvm, corepack, and avoiding global chaos)
Install Node in WSL using nvm. I don’t care if apt has nodejs. I don’t care if Windows has Node already. You want fast, repeatable,
per-user versions that won’t collide with system packages or corporate tooling.
Version policy that doesn’t create pager fatigue
- Use Active LTS for most teams.
- Use current only when you’re validating ahead of time, not because you felt lucky.
- Pin Node major/minor in
.nvmrcat the repo root. That’s your contract.
Package managers: let Corepack do its job
Modern Node setups often use Yarn or pnpm. Corepack ships with Node and can install and pin the package manager version declared by the project.
This reduces “works on my machine” because “my machine” ran Yarn 1 while CI ran Yarn 3.
Global npm installs are fine for a small set of things (linters, scaffolding) but prefer project-local binaries and npx or scripts.
Global installs become a shared junk drawer with no labels.
Python in WSL (pyenv, venv, and compiled dependencies)
Python is the runtime that will punish vague intentions. Install with pyenv when you need multiple versions (you do), and use venv
per project. Don’t install random pip packages globally and then wonder why one project’s dependencies sabotage another.
System dependencies: the part everyone forgets
Many Python packages include native extensions (cryptography, numpy, lxml). Wheels often exist, but not always for your Python version or architecture.
When pip builds from source, you need system headers and compilers. That’s why the “simple pip install” sometimes becomes a C toolchain install.
This is normal. It’s just not pleasant.
Two rules that eliminate most Python drama
- Always use a venv and keep it inside the project or under a dedicated directory.
- Pin dependencies with a lock approach appropriate to your org (requirements.txt with hashes, pip-tools, poetry, etc.). The mechanism matters less than the discipline.
Go in WSL (GOVERSION, GOPATH, and module hygiene)
Go is the least dramatic of the three, which is why people get complacent and do weird things like manually unpack tarballs into random directories.
Install Go in WSL in a predictable location and keep your environment variables boring.
Modules-first worldview
If you’re building modern Go, use modules. Put code wherever you want (under your home directory is fine), and let go env tell you what matters.
GOPATH still exists and still affects caches and older tools, but it’s no longer the place your source code must live.
Go version strategy
If your org ships multiple Go services, you will eventually need multiple Go versions. You can use a Go version manager, or you can standardize per quarter.
What you shouldn’t do is “whatever Go was installed on the laptop that day.”
Interop without pain: PATH, Git, SSH, and VS Code
The cleanest setup is: languages inside WSL, editor on Windows, and a minimal interop layer between them. VS Code’s WSL integration is popular because it
doesn’t pretend the boundary doesn’t exist—it just makes it less annoying.
Git: pick one side and be consistent
Use Git inside WSL for repos stored inside WSL. If you use Windows Git on a repo inside WSL, you’re inviting line-ending weirdness and permission surprises.
If you use WSL Git on a repo under /mnt/c, you’ll likely get performance issues and occasional metadata confusion.
SSH keys: don’t copy them around casually
Store keys inside WSL (~/.ssh) and protect them with proper permissions. If you must use Windows-managed keys, be explicit about the agent strategy.
The “it worked yesterday” class of SSH problems often comes from agent confusion across the boundary.
Joke #2: Nothing ages faster than an SSH key you pasted into a chat “just for a minute.”
Three corporate mini-stories (how teams actually break this)
Incident: the wrong assumption (PATH inheritance and the phantom Python)
A team migrated a mid-sized Node + Python monorepo to WSL to get closer to Linux production. People were happy for about a week.
Then CI started failing for one developer only, with Python tooling errors that looked like bad dependencies. Everyone did the usual dance:
delete venv, reinstall, clear caches, swear quietly.
The root cause wasn’t the repo. It was an assumption: “If I’m in WSL, python is Linux Python.” On that machine, WSL inherited Windows PATH,
and python.exe from Windows was getting called first. It didn’t always fail; it failed when scripts passed Linux paths or relied on Linux-only libraries.
The error messages were misleading because the wrong interpreter was running.
The fix was boring: disable Windows PATH injection in /etc/wsl.conf, restart WSL, and verify interpreter identity with which python
and python -c checks. After that, their setup docs explicitly included “prove your Python is Linux Python” as a required step.
The lesson: when a failure is machine-specific and inconsistent, suspect the boundary—PATH, filesystem location, proxy settings—before suspecting dependencies.
Dependencies are deterministic; your environment often isn’t.
Optimization that backfired (moving repos to /mnt/c to “simplify backups”)
Another org decided it would be “cleaner” if all source code lived under the Windows user profile so it would be included in corporate backups and endpoint DLP scans.
Their WSL distro was treated as disposable. That sounded reasonable in a meeting.
The first complaint was slow installs: npm ci taking forever. Then Go builds started dragging. Python venv creation became sluggish.
Engineers started switching back to Windows-native tooling “just for speed,” which reintroduced the exact “two environments” mess they were trying to avoid.
Security agents on Windows were scanning file operations, which multiplied the pain for the “many small files” workloads. Node’s dependency tree is essentially
a benchmark for filesystem overhead, and it failed the benchmark loudly. The team tried to optimize by excluding directories from scanning, but policy exceptions were slow,
and the exclusions didn’t cover every path.
They eventually flipped the model: repos and dependency directories live in WSL’s filesystem for performance; backup is handled by pushing to remote git and,
when needed, exporting artifacts. DLP concerns were addressed by controlling what can be copied out of WSL rather than punishing every read/write.
The lesson: optimizing for governance by moving hot build workloads onto the Windows filesystem is like putting your database on network storage “for convenience.”
It will work. It will also be slow. And then people will work around it in ways you don’t control.
Boring but correct practice that saved the day (pinning toolchains and verifying them)
A platform team supported dozens of services across Node, Python, and Go. They didn’t want bespoke snowflake laptops. They wrote a bootstrap script that:
installed base packages, configured WSL boundaries, installed nvm/pyenv, and set default versions. They also added a “verify” step that printed versions and key paths.
It wasn’t flashy. It didn’t use a fancy provisioning framework. It was a shell script and a checklist. It also forced standard outputs: everyone’s
node, python, pip, and go were checked the same way. That meant support tickets began with facts, not vibes.
When a new corporate laptop image rolled out, it quietly changed Windows PATH ordering and introduced a Windows Python shim that confused WSL users—on some machines.
The bootstrap verification caught it immediately because the output didn’t match expected patterns. Instead of a month of intermittent issues, it was a one-day fix:
update /etc/wsl.conf guidance and re-run the verification step.
The lesson: the boring practice is “pin versions and verify identity.” Not once, but every rebuild. It’s dull until it saves you, and then it’s your favorite.
Fast diagnosis playbook
When “Node/Python/Go in WSL is broken” lands in your lap, don’t start reinstalling things. You’ll destroy evidence and waste time.
Diagnose in this order to find the bottleneck fast.
First: confirm the boundary (WSL version, PATH, filesystem location)
- Is this WSL 2?
- Is the repo under Linux filesystem or
/mnt/c? - Is WSL inheriting Windows PATH? Are Windows binaries shadowing Linux ones?
Second: confirm toolchain identity (which binary, which version, which install method)
- For Node:
which node,node -v,nvm current - For Python:
which python,python -V,python -c "import sys; print(sys.executable)" - For Go:
which go,go version,go env
Third: confirm performance constraints (I/O, CPU, memory, antivirus interaction)
- If installs/builds are slow: check if you’re operating on
/mnt/c. - If random hangs: check free disk space and memory pressure inside WSL.
- If network fetches fail: check proxy settings and DNS resolution inside WSL.
Fourth: only then reinstall
Reinstalling without knowing what went wrong is how you end up with three broken installs instead of one.
Common mistakes: symptoms → root cause → fix
1) “python points to python.exe”
Symptoms: python runs but can’t import Linux modules; paths look like C:\...; venv behaves strangely.
Root cause: WSL inherited Windows PATH; Windows Python is first in precedence.
Fix: Disable PATH append in /etc/wsl.conf, restart WSL, verify with which python and file $(which python).
2) “npm install takes forever”
Symptoms: npm ci or pnpm install is dramatically slower than teammates; high CPU in Windows security processes.
Root cause: Repo or node_modules under /mnt/c; Windows filesystem + scanning overhead for tiny files.
Fix: Move repo into WSL filesystem (~/src), reinstall dependencies, keep Windows access for editing only.
3) “pyenv install fails with missing headers”
Symptoms: Build errors mentioning zlib, openssl, readline, or ffi.
Root cause: Missing build dependencies for compiling Python.
Fix: Install required packages via apt (compiler, dev headers), then retry pyenv install.
4) “go get works, but builds fail on CI”
Symptoms: Local build succeeds; CI or teammate build fails; module versions differ.
Root cause: Module versions not pinned/committed; go.mod/go.sum drift.
Fix: Run go mod tidy, commit go.sum, and set a minimum Go version in go.mod.
5) “Permission denied in repo”
Symptoms: Git can’t write; tools can’t create files; weird UID/GID mismatches.
Root cause: Working in Windows-mounted filesystem with permission metadata mismatch; sometimes bad mount options.
Fix: Move repo into WSL filesystem; if you must use /mnt/c, tune mount options and accept limitations.
6) “VS Code terminal shows one Node, tasks use another”
Symptoms: Terminal prints Node 20; tasks/build scripts behave like Node 18; inconsistent results.
Root cause: Mixed contexts: Windows VS Code launching tasks in Windows shell vs WSL; or PATH differs between login/non-login shells.
Fix: Ensure you’re in a WSL Remote window; ensure shell init loads nvm/pyenv for non-interactive shells, or configure tasks explicitly.
Practical tasks (commands + outputs + decisions)
These are real checks I run. Each has: a command, what the output means, and the decision you make from it.
Run them inside WSL unless noted.
Task 1: Confirm WSL version and distros (from Windows)
cr0x@server:~$ wsl.exe -l -v
NAME STATE VERSION
* Ubuntu-22.04 Running 2
Debian Stopped 2
Meaning: Your primary distro is Ubuntu-22.04 and it’s WSL 2.
Decision: If VERSION is 1, convert it or install a WSL 2 distro. Don’t build modern toolchains on WSL 1 unless you enjoy edge cases.
Task 2: Check kernel and distro basics
cr0x@server:~$ uname -a
Linux cr0x-laptop 5.15.133.1-microsoft-standard-WSL2 #1 SMP Thu Oct 12 20:38:48 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
Meaning: You’re on the WSL2 kernel line; architecture is x86_64.
Decision: If you see unexpected architecture (e.g., arm64), ensure your language builds and binaries match.
Task 3: Detect Windows PATH leakage
cr0x@server:~$ echo "$PATH" | tr ':' '\n' | head -n 10
/home/cr0x/.nvm/versions/node/v20.11.1/bin
/home/cr0x/.pyenv/shims
/home/cr0x/.pyenv/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
Meaning: PATH starts with Linux user toolchains; no obvious /mnt/c/Windows entries in the first lines.
Decision: If you see many /mnt/c entries, disable PATH append in /etc/wsl.conf and restart WSL.
Task 4: Verify where Node comes from
cr0x@server:~$ which node
/home/cr0x/.nvm/versions/node/v20.11.1/bin/node
Meaning: Node is provided by nvm in your home directory.
Decision: If which node points to /usr/bin/node, you’re using apt’s Node. Decide if that’s acceptable; usually it’s not for multi-version work.
Task 5: Confirm Node and npm are coherent
cr0x@server:~$ node -v && npm -v
v20.11.1
10.2.4
Meaning: Node and npm versions are aligned for that Node release.
Decision: If npm is unexpectedly old/new, you may have PATH shadowing or a partial install.
Task 6: Check Corepack status (Yarn/pnpm control plane)
cr0x@server:~$ corepack --version
0.24.1
Meaning: Corepack exists and can manage Yarn/pnpm versions.
Decision: If missing, you’re likely on an older Node or a custom build; decide whether to upgrade Node or manage package managers manually.
Task 7: Verify Python identity and executable path
cr0x@server:~$ which python
/home/cr0x/.pyenv/shims/python
Meaning: Python is controlled by pyenv shims (good for multi-version).
Decision: If it points to /mnt/c/... or ends in .exe, you’re running Windows Python inside WSL. Fix PATH leakage.
Task 8: Confirm Python version and where it runs from
cr0x@server:~$ python -V
Python 3.12.2
cr0x@server:~$ python -c "import sys; print(sys.executable)"
/home/cr0x/.pyenv/versions/3.12.2/bin/python
Meaning: You’re running the pyenv-installed interpreter.
Decision: If sys.executable points somewhere surprising, stop and fix interpreter selection before touching dependencies.
Task 9: Create and validate a venv (proves pip isolation)
cr0x@server:~$ python -m venv .venv
cr0x@server:~$ source .venv/bin/activate
(.venv) cr0x@server:~$ which python
/home/cr0x/.venv/bin/python
Meaning: Your shell is using the venv interpreter; pip installs will go into the venv.
Decision: If which python doesn’t change after activation, your shell init is broken or you’re not activating correctly.
Task 10: Verify Go installation and environment
cr0x@server:~$ which go
/usr/local/go/bin/go
cr0x@server:~$ go version
go version go1.22.1 linux/amd64
Meaning: Go is installed in a standard location and reports the correct OS/arch.
Decision: If it reports windows/amd64, you somehow called Windows Go from WSL. That’s a boundary violation.
Task 11: Confirm Go module mode and key paths
cr0x@server:~$ go env GOPATH GOMOD GOCACHE
/home/cr0x/go
/home/cr0x/src/myservice/go.mod
/home/cr0x/.cache/go-build
Meaning: GOPATH is in your home; modules are active (GOMOD points to a go.mod); caches are in Linux space.
Decision: If GOMOD is empty inside a module repo, you’re not in the module directory or GO111MODULE behavior is odd; fix before debugging dependencies.
Task 12: Detect “repo is on /mnt/c” (performance red flag)
cr0x@server:~$ pwd
/mnt/c/Users/cr0x/source/myapp
Meaning: You’re building from the Windows filesystem mount.
Decision: If this repo contains Node or Python dependencies, move it to ~/src in WSL. Expect big speedups.
Task 13: Measure basic disk space (mysterious install failures love full disks)
cr0x@server:~$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/sdc 251G 198G 41G 83% /
Meaning: You have ~41G free in the distro filesystem.
Decision: If you’re near 100%, stop. Clean caches before blaming package managers.
Task 14: Check memory pressure (random build kills and slowdowns)
cr0x@server:~$ free -h
total used free shared buff/cache available
Mem: 7.8Gi 5.6Gi 410Mi 112Mi 1.8Gi 1.7Gi
Swap: 2.0Gi 1.4Gi 640Mi
Meaning: Available memory is low, swap is active.
Decision: Expect slow builds. Close memory-heavy apps, tune WSL memory limits, or reduce parallelism for installs/tests.
Task 15: Check DNS and outbound connectivity (package fetch failures)
cr0x@server:~$ getent hosts pypi.org
151.101.64.223 pypi.org
151.101.128.223 pypi.org
151.101.192.223 pypi.org
151.101.0.223 pypi.org
Meaning: DNS resolution works from inside WSL.
Decision: If resolution fails, troubleshoot WSL DNS/proxy before touching language installers.
Checklists / step-by-step plan
Plan A: clean, repeatable setup (recommended)
-
Confirm WSL 2 and pick one distro.
Usewsl.exe -l -v. If you see WSL 1, fix that first. -
Update base packages.
Run apt update/upgrade and install build essentials. This avoids “pip tried to compile something and died” later. -
Set boundaries in /etc/wsl.conf.
Disable Windows PATH injection unless you have a specific need. -
Create a Linux-native workspace.
Make~/src, clone repos there, and stop developing under/mnt/c. -
Install nvm, then Node LTS.
Add.nvmrcper repo. Prefercorepackfor Yarn/pnpm pinning. -
Install pyenv, then Python versions you need.
Set a global default and a per-repo local version when required. -
Use venv per project.
Create.venv, activate it, then install dependencies. -
Install Go in a stable path.
SetPATHto include Go, verifygo env. -
Run the verification tasks.
Checkwhichand versions for node/python/go, and check repo location. -
Automate it.
Put these steps into a bootstrap script and require it for onboarding.
Plan B: when you’re forced to work under /mnt/c (acceptable but slower)
- Keep language runtimes in WSL anyway. Don’t install parallel Windows runtimes “to speed things up.”
- Keep dependency-heavy directories out of
/mnt/cif possible (some tools support configuring cache and build output paths). - Expect slow Node installs. Plan for it: use lockfiles, avoid repeated clean installs, and don’t benchmark WSL based on this mode.
- Be explicit about line endings and executable bits; Windows filesystems don’t naturally preserve Linux semantics.
FAQ
Should I install Node/Python/Go on Windows as well?
Generally no. Install them in WSL. Installing them on Windows too creates ambiguity: editors, terminals, and scripts may pick different runtimes.
If you need Windows-native builds for a specific product, isolate that workflow and document it.
Is WSL 2 always better than WSL 1 for dev?
For most modern dev stacks, yes. WSL 2 is closer to real Linux behavior and usually better for containers and tooling compatibility.
WSL 1 can be useful for certain filesystem access patterns, but it’s not the default choice for clean toolchains.
Why is Node so much slower in some setups?
Because Node workloads stress filesystems: many small files, frequent metadata operations. If your repo is on /mnt/c,
you pay interop overhead plus whatever Windows security tooling is doing.
Can I use apt to install Node and Python instead of version managers?
You can, but you’ll trade simplicity today for pain later when versions diverge across projects. apt is fine when you need one stable version
and the distro provides what you need. Most teams need multiple versions. Use nvm and pyenv.
What about Conda for Python?
Conda can work well, especially for scientific stacks. In corporate environments, it can also introduce its own dependency universe and disk footprint.
If your org already standardizes on Conda, use it. If not, pyenv + venv is usually simpler and more Linux-native.
Where should I store my Git repositories?
Inside WSL’s Linux filesystem (e.g., ~/src) for performance and correct Linux semantics. Use Windows paths for occasional file sharing,
not for dependency-heavy builds.
How do I keep my environment reproducible across a team?
Pin tool versions in-repo (.nvmrc, Python version files, go.mod), commit lockfiles, and provide a bootstrap script that installs
the version managers and required packages. Add a verification step that prints paths and versions.
Do I need Docker if I use WSL?
Not always. WSL gives you a Linux userland that’s good for dev. Docker adds runtime isolation and production parity for services.
Many teams use both: WSL for the developer shell, Docker for services and dependencies.
What’s the safest way to handle SSH keys with WSL?
Keep keys in WSL under ~/.ssh, use strict permissions, and use an SSH agent inside WSL when possible.
Avoid copying keys between Windows and WSL casually. If corporate policy requires a Windows agent, document the integration explicitly.
Why do I see different behavior between terminal sessions?
Shell initialization differs between interactive and non-interactive shells, login vs non-login shells, and between terminal apps.
If nvm/pyenv initialization lives only in one file, your tools may vanish in another context. Standardize your shell init.
Next steps that keep it clean
If you want a WSL dev environment that doesn’t rot:
- Move active repos into
~/srcinside WSL and re-run installs. This alone fixes a shocking number of “WSL is slow” complaints. - Disable Windows PATH inheritance in WSL unless you’ve justified it with a real requirement.
- Adopt version managers: nvm for Node, pyenv for Python, and a consistent Go installation strategy. Pin versions per repo.
- Write a bootstrap script plus a verification script. Make “print versions and paths” the first step of debugging, not the last.
- When something breaks, follow the diagnosis order: boundary → identity → performance → reinstall. Don’t guess.
The goal isn’t to worship cleanliness. It’s to make your laptop behave like a small, predictable production system: controlled inputs, clear boundaries,
and failures that are explainable instead of mystical.