NVM: Install Node Version Manager and Switch Versions Safely
- NVM doesn't just switch Node versions β it switches entire environments. npm version, global packages, and native module binaries all change with it. One 'nvm use' call changes everything, which is exactly why it's powerful and why you need to know what's active before installing anything globally.
- The .nvmrc file is the difference between 'works on my machine' and 'works on every machine'. Commit it to every repo. It costs nothing and saves your team from version mismatch debugging sessions that always happen at the worst possible time.
- Global packages don't survive version switches. If your team suddenly can't find 'tsc' or 'nodemon' after a version switch, that's why. The fix is '--reinstall-packages-from' on install, or better yet, putting CLI tools in devDependencies so they're project-local and version-agnostic.
I've watched a developer nuke a client's staging environment at 11pm because they ran 'npm install -g' after upgrading Node globally and silently broke every other project on the machine. Not a config error. Not a bad package. Just the wrong Node version running the wrong code β and no way to roll back fast. That's the kind of pain NVM exists to prevent.
The problem is that Node.js moves fast. A project you started two years ago might need Node 14. Your new microservice was built on Node 20. Your company's legacy monolith flatly refuses to start on anything above Node 16 because of a deprecated crypto API. Installing Node the 'normal' way β downloading it from nodejs.org and running the installer β gives you exactly one version at a time, globally, for everything. The moment you need two versions for two projects, you're either Googling workarounds or breaking something.
By the end of this, you'll know how to install NVM from scratch on macOS or Linux, install multiple Node versions, switch between them per-project automatically, and never think about version conflicts again. You'll also understand the one file β .nvmrc β that makes your whole team use the same Node version without anyone having to say a word about it.
Why Global Node Installs Will Eventually Burn You
Before NVM existed, the only way to change your Node version was to uninstall it and reinstall a different one. Full stop. People wrote shell scripts to automate the pain. Others just pinned everything to whatever Node version happened to be on the machine that week and hoped for the best.
The catastrophic version of this plays out when you join a team. The repo has no documented Node version. You run 'npm install', get 47 peer dependency warnings, and your app crashes on startup with 'TypeError: crypto.createCipher is not a function' β because createCipher was removed in Node 16 and you're running 18. You spend three hours debugging what looks like a broken crypto library before someone on Slack says 'oh yeah, use Node 14 for that one'. Three hours.
Global installs also mean global packages like 'ts-node' or 'nodemon' get installed against one specific Node version. Upgrade Node globally and those binaries can silently stop working or, worse, keep working but behave differently. NVM solves this by isolating each Node version in its own directory. When you switch versions, you switch the entire environment β runtime, npm, and all globally installed packages for that version.
# io.thecodeforge β JavaScript tutorial # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # STEP 1: Install NVM using the official install script. # This downloads NVM and wires it into your shell automatically. # Always check https://github.com/nvm-sh/nvm for the latest version tag. # As of writing, the current release is v0.39.7. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # STEP 2: The install script appends these lines to your shell config. # If you use bash, this goes into ~/.bashrc # If you use zsh (default on modern macOS), this goes into ~/.zshrc # You can verify by opening the file and looking at the bottom. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm itself [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # Tab-completion for nvm commands # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # STEP 3: Reload your shell config so the changes take effect # in the CURRENT terminal session without opening a new window. # Replace .zshrc with .bashrc if you're on bash. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ source ~/.zshrc # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # STEP 4: Verify NVM is installed and on your PATH. # If this prints a version number, you're good. # If it prints nothing, your shell config didn't load β see the warning callout. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm --version
Installing Node Versions and Switching Between Them
NVM has a clean mental model: you install versions into its own directory (~/.nvm/versions/node/), and at any moment exactly one version is 'active' in your current shell session. Switching is instant β no download, no restart, no package reinstall β because you're just changing which directory your PATH points to.
Here's the real-world scenario where this matters. You're maintaining a payment integration service that runs on Node 14 (the client hasn't approved an upgrade β you know how it goes). You also have a new internal tool you're building on Node 20 LTS. Without NVM, you are constantly uninstalling and reinstalling. With NVM, you install both once and switch in under a second.
One thing people miss: 'nvm use' only changes the version for the current terminal session. Open a new terminal tab and you're back to the default. That's intentional β it means you can have different projects open in different tabs, each running their own Node version simultaneously. The way to make a version sticky across all sessions is 'nvm alias default'.
# io.thecodeforge β JavaScript tutorial # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # See all available Node.js versions you can install. # This list is long β pipe it through grep to filter for LTS versions, # which are the ones you should be running in production. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm ls-remote --lts # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Install specific Node versions. # You'll install as many as your projects need. # LTS = Long Term Support β stable, security-patched, production-safe. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm install 20 # Installs the latest Node 20.x.x release nvm install 18 # Installs the latest Node 18.x.x release nvm install 14.21.3 # Installs an exact patch version β useful when a # specific patch fixed a bug your app depends on # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # See every version you have installed locally. # The arrow (β) shows which one is currently active. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm ls # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Switch to a specific version for THIS terminal session only. # This is the command you'll run most often. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm use 18 # Confirm which version is now active node --version npm --version # npm version changes too β it's bundled with Node # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Set a default version that activates in every NEW terminal session. # Without this, every new tab starts with no active version (or the # last one you set as default β which might be wrong). # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm alias default 20 # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Jump to the version your current project's .nvmrc file specifies. # This is the command you run when you cd into a project repo. # If .nvmrc doesn't exist, this throws an error β see next section. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm use
# v14.21.3
# v18.20.2
-> v20.11.1
default -> 20 (-> v20.11.1)
lts/* -> lts/iron (-> v20.11.1)
# node --version after 'nvm use 18':
v18.20.2
# npm --version after 'nvm use 18':
10.5.0
The .nvmrc File: Make Your Whole Team Use the Right Node Version
Here's the part most tutorials skip, and it's the part that actually matters in a team setting. A .nvmrc file lives in the root of your project and contains exactly one thing: the Node version that project needs. When any developer runs 'nvm use' inside that directory, NVM reads the file and switches to the right version automatically. No Slack message. No README footnote nobody reads. No 'it works on my machine'.
This is particularly important for CI/CD pipelines. Every GitHub Actions or Jenkins job that checks out your repo should call 'nvm use' before running any Node commands. Otherwise your pipeline runs whatever Node version happens to be installed on the build agent β which can drift over time as agents get updated. I've seen a CI agent upgrade Node from 18 to 20 during a routine maintenance window and break a production build at 6am because one package used a deprecated API that Node 20 finally removed.
You can also configure NVM to auto-switch when you change directories. This requires a small addition to your shell config. It's optional, but once you try it you won't go back β you just 'cd' into a project and the right Node version is already active.
# io.thecodeforge β JavaScript tutorial # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # CREATING A .nvmrc FILE FOR YOUR PROJECT # Run this from your project root directory. # This writes the current active Node version into .nvmrc. # Commit this file β it belongs in source control. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ node --version > .nvmrc # Verify what was written β should be something like 'v20.11.1' cat .nvmrc # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Or write a specific version manually: # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ echo "20.11.1" > .nvmrc # NVM accepts versions with or without the 'v' prefix # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # When any developer clones your repo and runs 'nvm use', # NVM reads .nvmrc and switches to the right version. # If they don't have it installed yet, they'll get: # N/A: version "20.11.1" is not yet installed. # The fix: run 'nvm install' (no version arg) β it reads .nvmrc too. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm install # installs the version from .nvmrc if not already present nvm use # then activates it # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # AUTO-SWITCHING: make NVM change Node version automatically # when you cd into a directory containing a .nvmrc file. # Add this block to your ~/.zshrc (or ~/.bashrc for bash users). # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Paste this into your shell config file: autoload -U add-zsh-hook # zsh only β loads the hook utility # This function runs every time you change directories load-nvmrc() { local nvmrc_path nvmrc_path="$(nvm_find_nvmrc)" # NVM's own function to find the nearest .nvmrc if [ -n "$nvmrc_path" ]; then # if a .nvmrc was found... local nvmrc_node_version nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")") # resolve installed version if [ "$nvmrc_node_version" = "N/A" ]; then nvm install # version not installed yet β install it elif [ "$nvmrc_node_version" != "$(nvm version)" ]; then nvm use # version installed but not active β switch fi elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ]; then nvm use default # left a .nvmrc directory β restore default fi } add-zsh-hook chpwd load-nvmrc # wire the function to the directory-change hook load-nvmrc # also run it once on shell startup
v20.11.1
# When you cd into a project directory with .nvmrc containing 'v18.20.2':
Found '/path/to/your-project/.nvmrc' with version <v18.20.2>
Now using node v18.20.2 (npm v10.5.0)
# When you cd out to a directory without .nvmrc:
Restored to default Node version: v20.11.1
Managing Global Packages Per Node Version and Keeping Things Clean
Here's what surprises most developers switching to NVM for the first time: global packages don't carry over between Node versions. If you install 'npm install -g typescript' while on Node 20, then switch to Node 18, TypeScript is gone from that version's global scope. Each Node version in NVM has its own isolated node_modules directory for globals.
This is actually correct behaviour, not a bug. A TypeScript version compiled for Node 20 might behave differently on Node 14. Isolation prevents that silent mismatch. But it does mean you need to reinstall globals when you set up a new version. NVM has a shortcut for this: the '--reinstall-packages-from' flag copies all globally installed packages from one version to another during install.
Also know when to uninstall old versions. Unused Node versions sit in ~/.nvm/versions/ and each one takes around 50-100MB of disk space including its npm cache. After six months of grabbing versions you 'needed just to test something', you can easily have a gigabyte of stale Node installs. 'nvm uninstall' cleans them out. Run 'nvm ls' occasionally and prune anything you haven't touched in months.
# io.thecodeforge β JavaScript tutorial # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # REINSTALLING GLOBAL PACKAGES FROM ANOTHER VERSION # When you install Node 20 and want all the globals you had on Node 18, # pass the --reinstall-packages-from flag during install. # NVM copies the package list from the source version and installs each one fresh. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm install 20 --reinstall-packages-from=18 # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Check which global packages are installed for the CURRENT active version. # This scope flag (-g) is crucial β without it you see project-local packages. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ npm list -g --depth=0 # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # UNINSTALLING A NODE VERSION YOU NO LONGER NEED # You cannot uninstall the version that's currently active. # Switch to a different version first, then uninstall. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm use 20 # Switch away from the version you want to remove nvm uninstall 14.21.3 # Remove Node 14.21.3 and all its global packages # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # ALIASING VERSIONS: give a version a memorable name # Useful when you're jumping between a 'client-legacy' version # and your standard dev version constantly. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ nvm alias payments-service 18.20.2 # create a named alias nvm use payments-service # switch using the alias name nvm alias --delete payments-service # remove the alias when done # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # CHECKING WHICH VERSION A PROJECT SHOULD USE # before running 'nvm use', see what version .nvmrc specifies # without actually switching. Useful in scripts. # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ cat .nvmrc # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # FULL CLEANUP: see how much disk space your NVM installations are using # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ du -sh ~/.nvm/versions/node/* # shows size of each installed version
/home/user/.nvm/versions/node/v20.11.1/lib
βββ npm@10.5.0
βββ typescript@5.3.3
βββ nodemon@3.0.3
# nvm uninstall 14.21.3:
Uninstalled node v14.21.3
# du -sh output example:
98M /home/user/.nvm/versions/node/v14.21.3
112M /home/user/.nvm/versions/node/v18.20.2
108M /home/user/.nvm/versions/node/v20.11.1
| Feature / Aspect | Global Node Install (nodejs.org) | NVM |
|---|---|---|
| Multiple Node versions simultaneously | No β one version at a time, globally | Yes β unlimited versions, all isolated |
| Switching versions | Uninstall and reinstall β 5-10 minutes | nvm use 18 β under 1 second |
| Per-project version pinning | Not possible without manual PATH hacks | Automatic via .nvmrc file |
| Global package isolation | One shared global scope for all projects | Each Node version has its own global scope |
| CI/CD compatibility | Depends entirely on the build agent's Node version | nvm install + nvm use in pipeline script |
| Windows support | Full native support | NVM for Unix/macOS only β use nvm-windows on Windows |
| Team consistency enforcement | README note that nobody reads | .nvmrc committed to git β enforced automatically |
| Requires sudo/admin rights | Yes β installs into system directories | No β installs entirely in your home directory |
| Rollback to previous version | Full uninstall + reinstall required | nvm use [previous-version] β instant |
| Disk footprint | Single install, minimal | ~100MB per version β prune unused ones regularly |
π― Key Takeaways
- NVM doesn't just switch Node versions β it switches entire environments. npm version, global packages, and native module binaries all change with it. One 'nvm use' call changes everything, which is exactly why it's powerful and why you need to know what's active before installing anything globally.
- The .nvmrc file is the difference between 'works on my machine' and 'works on every machine'. Commit it to every repo. It costs nothing and saves your team from version mismatch debugging sessions that always happen at the worst possible time.
- Global packages don't survive version switches. If your team suddenly can't find 'tsc' or 'nodemon' after a version switch, that's why. The fix is '--reinstall-packages-from' on install, or better yet, putting CLI tools in devDependencies so they're project-local and version-agnostic.
- The counterintuitive truth: NVM installs entirely in your home directory with zero sudo privileges required. That's not a quirk β it's intentional. System-level Node installs require admin rights and pollute shared system directories. NVM keeps everything in ~/.nvm and that's the right design for a per-user tool.
β Common Mistakes to Avoid
- βMistake 1: Running 'nvm use' in a subshell or script and expecting it to affect the parent shell β the terminal prompt still shows the old version. NVM version switches only apply to the current shell process. A script that calls 'nvm use 18' affects nothing outside that script. To fix: source the script instead of executing it ('source ./setup.sh'), or set the version in your shell profile before running the script.
- βMistake 2: Forgetting that 'nvm use' is session-scoped β opening a new terminal tab after switching to Node 18 drops you back to the default version, causing 'node: /lib/x86_64-linux-gnu/libc.so.6: version GLIBC_2.28 not found' type errors if the default is incompatible with installed native modules. Fix: run 'nvm alias default 18' to persist the version across all new sessions, or use .nvmrc + auto-switching so every project sets itself up.
- βMistake 3: Installing NVM while a system Node.js install still exists on PATH β 'nvm --version' works but 'node --version' still returns the system version because the system Node is earlier in PATH. The symptom is that 'which node' returns '/usr/bin/node' instead of something inside '~/.nvm/'. Fix: remove the system Node installation completely ('sudo apt remove nodejs' on Ubuntu or uninstall via the nodejs.org package on macOS), then reload your shell.
- βMistake 4: Committing node_modules built under one Node version and deploying to a server running a different version β native addons like 'bcrypt' or 'sharp' will throw 'Error: The module was compiled against a different Node.js version' at runtime. Fix: never commit node_modules, add it to .gitignore, and always run 'npm ci' on the target server after switching to the correct version via .nvmrc.
Interview Questions on This Topic
- QIf two developers on your team both run 'nvm use' in the same project directory but get different active Node versions, what are the possible causes and how do you diagnose it?
- QYour CI pipeline runs 'nvm use' inside a shell script but the Node version doesn't change for subsequent commands in the pipeline. What's happening and how do you fix it without restructuring the whole pipeline?
- QWhen would you choose a Docker-based Node version strategy over NVM in a team environment, and what specific team or project conditions would push you toward Docker?
- QA junior developer reports that 'nodemon' suddenly stopped working after they ran 'nvm install 20'. They can still run 'nodemon' on Node 18. What do you tell them, and what's the root cause?
Frequently Asked Questions
Does NVM work on Windows?
The original NVM (nvm-sh) only works on macOS and Linux. On Windows, use nvm-windows β a completely separate project maintained by coreybutler. The commands are almost identical ('nvm install', 'nvm use', 'nvm ls') but the internals are different and some edge-case behaviours differ. WSL2 (Windows Subsystem for Linux) is another solid option β install the Linux version of NVM inside your WSL2 Ubuntu environment and it works identically to native Linux.
What's the difference between 'nvm install' and 'nvm use'?
'nvm install' downloads a Node version from the internet and stores it in ~/.nvm β you only need to run it once per version. 'nvm use' switches your active session to an already-installed version β it's instant because nothing downloads. The rule: install once, use every time you switch. If you run 'nvm use 20' and get 'N/A: version not installed', you skipped the install step.
How do I make NVM automatically switch Node versions when I cd into a project folder?
Add the auto-switching hook to your shell config (~/.zshrc for zsh, ~/.bashrc for bash). The hook is a function that fires on directory change, checks for a .nvmrc file, and calls 'nvm use' if one exists. The full function is documented in the nvm-sh README under 'Deeper Shell Integration'. After adding it, run 'source ~/.zshrc' to activate without restarting the terminal.
Why do global npm packages disappear when I switch Node versions with NVM?
Each Node version in NVM has its own isolated node_modules directory for global packages at ~/.nvm/versions/node/vX.X.X/lib/node_modules/. This is by design β a native module compiled for Node 18 can corrupt silently or crash loudly when loaded by Node 20. When you install a new version, use 'nvm install [version] --reinstall-packages-from=[old-version]' to copy globals across. Long-term, move project CLI tools into devDependencies so they're version-pinned at the project level and don't depend on globals at all.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.