TechEarl

How to Update Node.js: nvm, fnm, Volta, Direct Install (2026)

Every reliable way to update Node.js on Linux, macOS, and Windows. Covers nvm, fnm, Volta, n, the nodejs.org installer, apt/brew/winget, Docker, GitHub Actions, per-project pinning, and the rebuild-native-modules step everyone forgets.

Ishan KarunaratneIshan Karunaratne⏱️ 18 min readUpdated
A developer terminal showing node --version output before and after running nvm install --lts, with arrows indicating the upgrade path between Node versions

Updating Node.js sounds like a one-liner until you realise the right answer depends on how you installed it in the first place: a version manager (nvm, fnm, Volta, n), the official nodejs.org installer, your OS package manager (apt, dnf, brew, winget, choco), or a Docker base image. Each path has a different upgrade command, a different way to pin the version per-project, and a different failure mode when native modules need rebuilding. Below is the full reference for every realistic upgrade route in 2026, with the version-manager comparison, per-project pinning patterns (.nvmrc, .node-version, engines, volta), CI/CD setup, and the troubleshooting list that catches the post-upgrade surprises.

Current Node.js
26.2.0

Latest release with newest features. Best for experimentation.

Latest LTS
24.16.0

Long-Term Support — the version to use in production.

How do I update Node.js to the latest version?

To update Node.js, first check your current version with node --version. If you installed Node via a version manager, upgrade with that tool: nvm install --lts && nvm use --lts for nvm, fnm install --lts && fnm use lts-latest for fnm, or volta install node@lts for Volta. If you installed from nodejs.org, download the latest LTS installer from nodejs.org/en/download and re-run it (it replaces the existing install). On Linux without a version manager, use NodeSource (curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs). On macOS Homebrew: brew upgrade node. On Windows: winget upgrade OpenJS.NodeJS or reinstall from nodejs.org. After upgrading, run node --version and npm --version to verify, then npm rebuild inside any project that uses native modules (bcrypt, sharp, node-sass, better-sqlite3) so they recompile against the new ABI.

Jump to:

Pre-flight: check your current Node version

Before any upgrade, capture the current state:

bash
node --version    # e.g. v18.19.0
npm --version     # e.g. 10.2.3
which node        # tells you which install is on PATH

which node (or where node on Windows) is the most important diagnostic. If it prints /usr/local/bin/node, you have a system install. If it prints ~/.nvm/versions/node/v18.19.0/bin/node, you are on nvm. If it prints ~/.volta/bin/node, you are on Volta. The upgrade path follows the install path.

If node is not found at all, you do not have Node installed yet, and any of the methods below will install it fresh.

LTS vs Current: which one to install

Node.js ships two release lines in parallel:

LineCadenceUse for
LTS (Active)Even-numbered majors (20, 22, 24), released April, supported 30 monthsProduction, libraries, anything you run for paying users
LTS (Maintenance)Previous Active LTS, security fixes only, ends at 36 months from releaseExisting prod systems mid-migration
CurrentOdd-numbered majors (21, 23, 25), released October, supported 6 monthsTrying new V8 features, testing the next LTS early

Pick LTS unless you have a specific reason to be on Current. Libraries on npm declare engines.node ranges that target LTS, and your CI matrix should mirror that. Current is fine for personal projects but you will be re-upgrading every six months as the line goes end-of-life.

For real-time current versions, the Node Versions card at the top of this page is fetched live from the Node.js release feed.

Version manager comparison

ToolPlatformsSpeedAuto-switchingPinning fileBest for
nvmLinux, macOS (Bash)Slow on shell startupManual or nvm use.nvmrcThe default; tons of tutorials reference it
nvm-windowsWindows onlyOKManualNoneWindows users wanting nvm syntax
fnmLinux, macOS, WindowsVery fast (Rust)Yes (via shell hook).nvmrc or .node-versionAnyone tired of nvm slowness
VoltaLinux, macOS, WindowsFastYes (per-project, transparent)package.json "volta" keyTeams who want repo-pinned Node + package manager versions
nLinux, macOSFastManualNone nativelySimple use-cases, no shell init hook needed
asdfLinux, macOSOKYes (.tool-versions).tool-versionsPolyglot devs managing Node + Ruby + Python + Go in one tool

If you cannot decide: fnm for a personal machine, Volta for a team monorepo where you want everyone on the same version automatically.

Method 1: nvm (Linux and macOS)

nvm is the most widely documented Node version manager. It is a Bash script that shims node, npm, and npx to whichever installed version is currently selected.

Install nvm (or update to the latest release):

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Reload your shell, or:
source ~/.bashrc       # or ~/.zshrc on macOS

List available versions and install LTS:

bash
nvm list-remote --lts          # see all LTS releases
nvm install --lts              # install the latest LTS
nvm install 22.11.0            # or a specific version
nvm install node               # the absolute latest (Current)

Switch versions and set a default:

bash
nvm use --lts                  # this shell only
nvm alias default --lts        # default for new shells
nvm current                    # show the active version
nvm ls                         # list locally installed versions

Per-project switching with .nvmrc:

bash
echo "22" > .nvmrc             # or "lts/iron", "22.11.0"
nvm use                        # reads .nvmrc in the cwd

To auto-switch when you cd into a project, add a shell hook to your .bashrc or .zshrc (the nvm README has the snippet). The default nvm cd hook is slow because it spawns a subshell; fnm and Volta both do this faster.

To remove an old version after the upgrade:

bash
nvm uninstall 18.19.0

Method 2: fnm (fast, cross-platform)

fnm (Fast Node Manager) is a Rust rewrite of nvm. Same conceptual model, dramatically faster shell startup, works on Windows in addition to Linux and macOS.

Install fnm:

bash
# Linux / macOS
curl -fsSL https://fnm.vercel.app/install | bash

# Windows (PowerShell)
winget install Schniz.fnm

# macOS via Homebrew
brew install fnm

Add the shell hook (this is what enables auto-switching on cd):

bash
# ~/.zshrc or ~/.bashrc
eval "$(fnm env --use-on-cd)"

# PowerShell ($PROFILE)
fnm env --use-on-cd | Out-String | Invoke-Expression

Install and use:

bash
fnm install --lts
fnm install 22.11.0
fnm use lts-latest             # or a specific version
fnm default lts-latest         # default for new shells
fnm list                       # local
fnm list-remote                # remote

Per-project pinning works with both .nvmrc and .node-version files:

bash
echo "22" > .node-version      # fnm reads either
cd into-this-dir-and-watch     # fnm auto-switches

The --use-on-cd hook makes fnm read the pinning file every time you change directories. No conscious step, no forgotten version.

Method 3: Volta (project-pinned via package.json)

Volta takes a different approach: instead of a global "active version", every project declares its own Node and package manager versions in package.json, and Volta transparently switches when you run a command in that directory.

Install Volta:

bash
# Linux / macOS
curl https://get.volta.sh | bash

# Windows
winget install Volta.Volta

Install Node and set the project version:

bash
volta install node@lts         # globally available
volta install node@22.11.0
volta install npm@10           # pin npm version too

# Inside a project:
volta pin node@22              # writes "volta" key into package.json
volta pin npm@10

This adds to package.json:

json
{
  "volta": {
    "node": "22.11.0",
    "npm": "10.9.0"
  }
}

Every developer (and CI) who runs node or npm inside that repo gets exactly those versions, automatically. No shell hooks, no remembering to run nvm use. This is the best option for teams because the version pinning lives in source control where everyone benefits.

The catch: Volta does not manage Yarn 2+ Berry or pnpm out of the box as cleanly as it manages Node and classic npm/Yarn. For pnpm, see the Volta pnpm docs.

Method 4: n (simple alternative to nvm)

n is a minimalist version manager: no shell init hook, no PATH manipulation beyond installing to /usr/local. Originally written by TJ Holowaychuk.

bash
# Install n itself (requires Node already installed, or use n-install)
npm install -g n

# Install the latest LTS
sudo n lts

# Install latest current
sudo n current

# Install a specific version
sudo n 22.11.0

# List installed versions
n ls

# Switch interactively (arrow keys)
sudo n

n installs Node binaries to /usr/local/n/versions/node/<version> and symlinks the active one to /usr/local/bin/node. Because it touches /usr/local, you need sudo (or a writable prefix via N_PREFIX).

n is appealing because it has no shell-startup overhead, but it lacks per-directory auto-switching. If you only ever work on one Node project at a time, n is plenty.

Method 5: Direct download from nodejs.org

For machines where you cannot or do not want to install a version manager (locked-down corporate laptops, kiosks, single-purpose servers), the official installer from nodejs.org/en/download is the right answer.

  1. Visit nodejs.org/en/download
  2. Choose LTS unless you need Current
  3. Pick the right installer for your OS and architecture (Windows .msi, macOS .pkg Universal/Intel/Apple Silicon, Linux tar.xz)
  4. Run it; it replaces any existing install of the same major
  5. Open a new terminal (the old one has the stale PATH) and verify:
bash
node --version
npm --version

The macOS .pkg installer is universal as of Node 16+ and runs natively on both Intel and Apple Silicon. The Windows .msi adds Node to PATH and ticks the "install build tools" option if you want native-addon compilation.

This method does not support side-by-side versions. To switch between majors after using the installer, install a version manager and let it take over.

Method 6: OS package managers (apt, dnf, brew, winget)

OS package managers ship Node too, but their versions usually lag behind the official release line by months. They are fine for ad-hoc scripts and Docker base images, but most production Node apps want a specific version that the OS repo does not have.

Ubuntu / Debian (apt) via NodeSource, which mirrors the official line:

bash
# Latest LTS
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs

# A specific major (e.g. Node 22)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs

RHEL / Fedora / Rocky / Alma (dnf) via NodeSource:

bash
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -
sudo dnf install -y nodejs

macOS Homebrew:

bash
brew update
brew install node            # latest stable
brew install node@22         # specific major
brew upgrade node            # upgrade in place
brew unlink node && brew link --overwrite node@22   # switch active major

Windows winget:

powershell
winget install OpenJS.NodeJS.LTS
winget upgrade OpenJS.NodeJS.LTS

Windows Chocolatey:

powershell
choco install nodejs-lts
choco upgrade nodejs-lts

The downside of every package-manager route is that you cannot easily switch back if the new version breaks something. If that matters, use a version manager.

Method 7: Windows specifics

Windows has the most install options because nvm itself does not run natively on Windows.

ToolNotes
nvm-windowsDifferent project from Unix nvm. Similar commands, runs natively. See github.com/coreybutler/nvm-windows.
fnmBest modern choice on Windows. winget install Schniz.fnm. Same commands as Unix.
VoltaNative Windows installer, repo-pinned versions via package.json.
wingetBuilt-in. winget install OpenJS.NodeJS.LTS. No version switching.
Chocolateychoco install nodejs-lts. Common on dev-heavy Windows machines.
Direct .msinodejs.org installer. Optional "build tools" checkbox installs Python and Visual Studio C++ workloads for native addons.

Install only one Node manager on a Windows machine at a time. They all manipulate PATH and the active node.exe shim differently, and combining (e.g. nvm-windows + winget Node) leads to "which node is on PATH" mysteries that take an hour to unwind.

For PowerShell auto-switching with fnm, add this to $PROFILE:

powershell
fnm env --use-on-cd | Out-String | Invoke-Expression

Method 8: Docker base images

Inside Docker, "updating Node" means changing the FROM line.

dockerfile
# Pin to an LTS major. Recommended for production.
FROM node:22-alpine

# Pin to a specific patch for fully-reproducible builds.
FROM node:22.11.0-alpine

# Convenience tag tracking whatever the current LTS is.
FROM node:lts-alpine

# Bigger but with more system libraries available.
FROM node:22-bookworm-slim

The node: images are built and published by the Node Docker team. Use -alpine for the smallest footprint (musl libc, ~50 MB) or -slim if you need glibc compatibility (closer to ~70 MB).

To upgrade an existing service:

  1. Change FROM node:20-alpine to FROM node:22-alpine in the Dockerfile
  2. Rebuild: docker build --no-cache -t myapp:22 .
  3. Run the test suite against the new image
  4. Pay attention to native-addon rebuild output during npm install (see After updating: rebuild native modules)

In production, always pin to a major or major.minor, never node:latest. The latest tag follows Current, not LTS, and the next October flip-day will surprise you.

For multi-arch (Apple Silicon dev, x86 prod), pass --platform=linux/amd64 or use Buildx with --platform linux/amd64,linux/arm64. The node: images are multi-arch by default.

CI/CD: GitHub Actions setup-node

The standard pattern is the actions/setup-node action with a matrix:

yaml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [20.x, 22.x, 24.x]
    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - run: npm ci
      - run: npm test

cache: 'npm' enables npm cache restoration keyed on package-lock.json — the single biggest speedup for any Node CI.

To read the version from .nvmrc or .node-version (so CI matches what local dev uses):

yaml
- uses: actions/setup-node@v4
  with:
    node-version-file: '.nvmrc'
    cache: 'npm'

Or, for projects using Volta, the action reads package.json:

yaml
- uses: actions/setup-node@v4
  with:
    node-version-file: 'package.json'   # reads "volta.node"

For Docker-based CI, base the build off node:22-alpine and skip setup-node entirely.

Per-project version pinning

Pinning Node per project means every developer (and CI) ends up on the same major. Four ways, in order of how much they cover:

FileRead byWhat it pins
.nvmrcnvm, fnm, GitHub actions/setup-nodeNode only
.node-versionfnm, asdf, GitHub actions/setup-nodeNode only
package.json engines.nodenpm (warns on mismatch), yarn (errors), pnpm (errors with engine-strict=true)Min Node, advisory
package.json voltaVolta, GitHub actions/setup-nodeNode AND package manager

A real-world package.json snippet:

json
{
  "name": "my-app",
  "engines": {
    "node": ">=22.0.0 <23.0.0",
    "npm": ">=10.0.0"
  },
  "volta": {
    "node": "22.11.0",
    "npm": "10.9.0"
  }
}

engines is advisory unless paired with engine-strict=true in .npmrc:

ini
# .npmrc
engine-strict=true

With that flag, npm install will fail (not warn) when run on a Node version outside the range. This is the pattern teams want for libraries so that nobody accidentally publishes a build from an unintended Node.

After updating: rebuild native modules

This is the post-upgrade step most articles skip. Native Node modules (anything that compiles C/C++ via node-gyp) are tied to the Node ABI for the major version they were built against. After a major upgrade, those .node binaries no longer load, and you get errors like:

code
Error: The module '/path/to/binding.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 108. This version of Node.js requires NODE_MODULE_VERSION 115.

The fix:

bash
npm rebuild
# or, to be thorough:
rm -rf node_modules package-lock.json
npm install

Common native-module culprits to test specifically after a major upgrade:

  • bcrypt, argon2 (password hashing)
  • sharp (image processing)
  • better-sqlite3, sqlite3, node-sass (legacy)
  • canvas, node-canvas
  • node-pty, node-serialport
  • Any module shipping a binding.gyp

For Docker, the rebuild happens inside the image build because npm install runs against the new Node base. As long as you do not mount a host node_modules into the container, you are fine.

If the rebuild itself fails, you are usually missing build tools. On Linux: sudo apt-get install -y build-essential python3. On macOS: xcode-select --install. On Windows: re-run the Node .msi and tick "install tools for native modules", or npm install -g windows-build-tools (older approach).

Keep npm, yarn, and pnpm aligned

A Node major upgrade often ships a new bundled npm. Verify:

bash
node --version    # v22.x
npm --version     # 10.x bundled

To upgrade npm independently of Node:

bash
npm install -g npm@latest
npm install -g npm@10        # pin a major

For Yarn (the classic v1 line):

bash
npm install -g yarn@1

For Yarn 2+ (Berry), Yarn ships per-project via corepack:

bash
corepack enable               # one-time
corepack prepare yarn@4.5.0 --activate

For pnpm, the recommended path in 2026 is also corepack:

bash
corepack enable pnpm
corepack prepare pnpm@9.12.0 --activate

corepack is bundled with Node 16.10+ and is the official Node way to manage package-manager versions per-project (a packageManager field in package.json declares the exact pnpm/yarn version).

Troubleshooting common upgrade issues

node: command not found after install. PATH has not picked up the new install. Open a fresh terminal. If still missing, run which node || echo missing and check that the install directory (e.g. ~/.nvm/versions/node/v22.11.0/bin) is on $PATH. For nvm/fnm/Volta, make sure their shell-init line is in ~/.bashrc or ~/.zshrc.

Version manager does not auto-switch on cd. You have not added the hook. For fnm: eval "$(fnm env --use-on-cd)" in your shell rc file. For nvm: copy the auto-switching snippet from the nvm README into your rc file.

EACCES: permission denied errors after switching versions. npm global directory is owned by root from a previous sudo-install. Either change the npm prefix to a user-owned directory (npm config set prefix ~/.npm-global and add ~/.npm-global/bin to PATH), or wipe and reinstall via a version manager so npm globals live under the version manager's directory.

node-gyp errors during install. Native build tools missing. Linux: sudo apt-get install -y build-essential python3. macOS: xcode-select --install. Windows: re-run the Node .msi with build-tools option.

Old PATH lingering after Homebrew upgrade. Run brew doctor and follow its instructions. hash -r (bash/zsh) clears the shell command cache so it re-finds node.

Two version managers fighting. Symptom: which node prints a path you did not expect, or node --version and nvm current disagree. Pick one manager, uninstall the other (brew uninstall node, nvm uninstall, etc.), and reload the shell.

Docker container uses old Node despite Dockerfile change. You are running a cached image. Rebuild with docker build --no-cache or bump the tag (myapp:22) so the orchestrator pulls fresh.

Production server still serves old version after apt upgrade. systemd is running a long-lived process spawned with the old binary. sudo systemctl restart <service> to pick up the new Node.

For server-admin patterns adjacent to this (managing remote machines, exporting and importing SSH configs), see How to Export and Import PuTTY Settings. For the shell scripting toolkit you will write upgrade scripts in, Bash For Loops and Bash While Loops cover the iteration patterns.

Modern alternatives: Bun and Deno

If you are upgrading Node anyway, it is worth knowing what else is in the runtime market in 2026.

RuntimeCompatible with Node?When to consider
BunMostly yes (Node API + npm)New projects wanting faster startup and bundled tooling (test runner, bundler, SQLite client). Drop-in for many Node scripts.
DenoYes via npm: specifiers and Node compatibility modeGreenfield TypeScript projects, secure-by-default sandboxing, single-binary deploys.
Node.js(itself)Everything else: largest ecosystem, longest track record, mature LTS, every npm library tested against it first.

For an existing Node app, the answer is almost always "upgrade Node, do not switch runtime". For a fresh CLI or experimental service, Bun and Deno are worth a 30-minute spike. Their compatibility with native addons is the main caveat: if your app depends on sharp, better-sqlite3, or anything that ships a .node binary, the Node path is the safer bet for now.

What to do next

If you got this page open while debugging a production incident, the next steps usually look like:

FAQ

TagsNode.jsJavaScriptnvmfnmVoltaVersion ManagementCLIDockerGitHub ActionsDevOps
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Create an EBS volume with aws ec2 create-volume, attach it to a running EC2 instance, format with mkfs.ext4 or mkfs.xfs, mount it, and persist across reboots with a UUID-based /etc/fstab entry. Console, AWS CLI, and Terraform walkthroughs.

How to Add an EBS Volume to an EC2 Instance

Create an EBS volume, attach it to a running EC2 instance, format and mount it, and survive reboots with a UUID-based fstab entry. Console, AWS CLI, and Terraform walkthroughs plus the Nitro device-naming gotcha that trips everyone.

Connect to an AWS EC2 instance using plain SSH with a key pair, EC2 Instance Connect, AWS Systems Manager Session Manager, or an EC2 Instance Connect Endpoint for private instances. Default usernames, security group rules, and troubleshooting Permission denied and Connection timed out.

How to SSH into an AWS EC2 Instance

Connect to an EC2 instance four ways: plain SSH with a key pair, EC2 Instance Connect, Session Manager, and EC2 Instance Connect Endpoint. Default usernames, security group rules, and the troubleshooting matrix that fixes Permission denied and Connection timed out.