TechEarl

How to Update Node.js Version: nvm, fnm, Volta (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 Karunaratne⏱️ 24 min readUpdated
Share thisCopied
A developer terminal showing node --version output before and after running nvm install --lts, with arrows indicating the upgrade path between Node versions

Update Node.js with the tool that installed your active copy. For nvm, run nvm install --lts followed by nvm use --lts. For a Windows LTS installer managed by winget, use winget upgrade --id OpenJS.NodeJS.LTS --exact. On macOS, upgrade the Homebrew formula you actually installed. Verify the active executable before choosing a route.

Use an actively supported LTS release unless you deliberately need Current. After switching versions, check Node and npm, test the project, and rebuild incompatible native addons before deploying.

Current Node.js
26.10.0

Latest release with newest features. Best for experimentation.

Latest LTS
24.21.0

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

How to update Node JS to the latest version

First run node --version and node -p "process.execPath". Then choose the matching method below. These are alternatives; running every installer can leave several conflicting Node copies on PATH.

Existing installationUpgrade routeCheck before switching
nvm on macOS/Linuxnvm install --lts, then nvm use --ltsA project .nvmrc can select a different version
fnm on Windows/macOS/Linuxfnm install --lts, then fnm use --ltsInitialize the fnm shell environment first
Windows winget LTS packagewinget upgrade --id OpenJS.NodeJS.LTS --exactConfirm it appears in winget list
Homebrewbrew update, then brew upgrade node or your installed node@MAJOR formulaUnversioned node follows the newest stable release, not an LTS-only policy
Official Node installerInstall the supported LTS package for your OS and architectureReopen the terminal and check PATH
Linux distribution packageUpgrade nodejs through the configured repositoryCheck which major that repository supplies

For a specific target major, use a version-manager command such as nvm install 24 or fnm install 24, then select it and run the project tests. Updating npm with npm install -g npm does not update the Node runtime.

Jump to:

Pre-flight: check your current Node version

Before any upgrade, capture the current state:

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

which node is the most important diagnostic. On Windows it is where node in Command Prompt, but in PowerShell where is an alias for Where-Object, so use where.exe node or Get-Command node there. If it prints /usr/local/bin/node, you have a system install. If it prints ~/.nvm/versions/node/v24.10.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, it may be absent or missing from this shell's PATH. Check the version-manager initialization before installing another copy.

LTS vs Current: which one to install

Node.js ships two release lines in parallel:

LineWhat it meansWhere things stand in September 2026
Active LTSThe release production should target. About 12 months in this state.Node 24 (Krypton), Active until 20 October 2026
Maintenance LTSThe previous LTS. Critical bug fixes and security only, for about 18 more months.Node 22 (Jod), until 30 April 2027
CurrentThe newest major, where new V8 features land first.Node 26, promoted to LTS on 28 October 2026
End of lifeNo fixes at all, security included.Node 20, 21, 23 and 25. Node 20 ended on 30 April 2026

Under the release schedule through Node 26, an even major is released in April or May as Current. Even-numbered majors are promoted to Active LTS that October, and that promotion is what makes them the safe production target. Support then runs about 36 months from release: roughly 12 months Active, then about 18 months Maintenance. Odd-numbered majors historically did not enter LTS. Starting with Node 27, Node is moving to an annual cycle in which every major is intended to enter LTS; see the current release policy.

If you are still on Node 20, you are on an end-of-life release. It stopped receiving security patches on 30 April 2026, and so did Node 25 on 1 June 2026. Those are the two upgrades worth doing today rather than this quarter.

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.

For the live numbers, the Node Versions card at the top of this page is fetched from the Node.js release feed and refreshes hourly. The official release schedule is the authority on what is supported on any given day.

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" keyExisting setups only. Unmaintained since 2025, see below
miseLinux, macOS, Windows (WSL)Very fast (Rust)Yes (.mise.toml, .node-version).mise.toml or .node-versionTeams wanting repo-pinned versions, and Volta's recommended successor
nLinux, macOSFastManual (n auto).n-node-version, .node-version, .nvmrcSimple 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, mise for a team monorepo where you want everyone on the same version automatically. I used to recommend Volta for that second case, and I no longer do, because its maintainers have stood it down.

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.7/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 24.10.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 "24" > .nvmrc             # or "lts/*", "24.10.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 20.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 24.10.0
fnm use --lts             # 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.

Read this before you install it. Volta's own README now opens with "Volta is unmaintained". The maintainers say existing installs should keep working for the foreseeable future, so there is no emergency, but they will not fix breakage from new OS releases, and they recommend migrating to mise. If you already run Volta, the section below still describes it accurately and you can stay put for now. If you are choosing a tool today, choose fnm or mise instead, and put the Volta migration on your roadmap.

The mise equivalents of the Volta commands below are short:

bash
mise use --global node@lts     # set the global default
mise use --pin node@lts        # pin this project, writes .mise.toml
mise upgrade node              # move to the newest matching release

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@24.10.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": "24.10.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 24.10.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 provides separate repositories for Node major versions:

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@24         # specific major
brew upgrade node            # upgrade in place
brew info node@24            # follow its PATH instructions if selecting this formula

Windows winget:

To upgrade Node via winget, run winget upgrade OpenJS.NodeJS (current line) or winget upgrade OpenJS.NodeJS.LTS (the LTS package). The two package IDs are OpenJS.NodeJS for the Current release and OpenJS.NodeJS.LTS for LTS, so pick the one matching what you have installed.

powershell
# Install (first time)
winget install OpenJS.NodeJS.LTS    # LTS package
winget install OpenJS.NodeJS        # Current package

# Upgrade in place
winget upgrade OpenJS.NodeJS        # Current
winget upgrade OpenJS.NodeJS.LTS    # LTS

winget upgrade only manages Node if winget (or the matching .msi) installed it in the first place. A Node that came from nvm-windows, fnm, Volta, or a raw archive is not tracked by winget, so winget upgrade will report nothing to update; upgrade those through their own tool instead. Check what owns your install with winget list OpenJS.NodeJS.

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 the Node patch; the image tag itself can still be rebuilt.
FROM node:24.10.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, pulling a fresh base image: docker build --pull --no-cache -t myapp:24 .
  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)

Choose the runtime major deliberately. Moving tags, including patch tags, are not immutable images; pin a reviewed digest when you need that guarantee and schedule updates for fixes. node:latest follows the newest stable line, which may be Current.

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: [22.x, 24.x]
    steps:
      - uses: actions/checkout@v7

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v7
        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@v7
  with:
    node-version-file: '.nvmrc'
    cache: 'npm'

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

yaml
- uses: actions/setup-node@v7
  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": "24.10.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, canvas
  • node-sass is end-of-life (July 2024) and will not build on a modern Node. Migrate to sass (Dart Sass) instead of trying to rebuild it.
  • 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" (current installers bundle them). Do not reach for the old npm install -g windows-build-tools package; it is deprecated and unmaintained.

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 use yarn@4.5.0       # pins it in package.json

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

bash
corepack enable pnpm
corepack use pnpm@9.12.0      # pins it in package.json

corepack is the official Node way to manage package-manager versions per project: a packageManager field in package.json declares the exact pnpm or yarn version, and corepack use writes it for you.

Two things changed recently and both bite on a fresh machine. Corepack is no longer bundled with Node. It shipped with Node from 14.19.0 up to but not including 25.0.0, so a clean Node 25 or 26 install has no corepack binary and you need npm install -g corepack before corepack enable. Node 24, the current LTS, still bundles it. And corepack prepare ... --activate is deprecated. Use corepack use <pkg>@<version> to pin a project, or corepack install -g <pkg>@<version> to set a system-wide default.

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/v24.10.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. If you are not using a version manager, point the npm prefix at a user-owned directory (npm config set prefix ~/.npm-global and add ~/.npm-global/bin to PATH). Do not do this under nvm. nvm's README states plainly that it is not compatible with the npm prefix option, and setting one breaks nvm use. Under nvm, fnm, or Volta the fix is the other way round: remove any custom prefix (npm config delete prefix) and let globals live inside the version manager's own 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 --pull (--no-cache alone reuses the base image you already have) 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.

This page is the hub of a small Node.js cluster. The companion guides go deeper on each step:

What to do next

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

FAQ

Run the upgrade command that matches how Node was installed. With a version manager: nvm install --lts, fnm install --lts, or volta install node@lts. With a package manager: brew upgrade node on macOS, winget upgrade OpenJS.NodeJS on Windows, or NodeSource on Linux (curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - && sudo apt-get install -y nodejs). From nodejs.org, download the latest LTS installer and re-run it.

To upgrade the node version to a specific major instead of the latest, replace --lts with the number (nvm install 24.11.0). Always run node --version afterwards to confirm the update took, and npm rebuild if the project uses native modules.

Node.js follows a strict release cadence: a new major even-numbered LTS line every April, supported for 30 months. The Node Versions card at the top of this page shows the current LTS and Current versions live from the Node.js release feed, so it is always accurate.

Pick LTS for production. The matching Maintenance LTS (previous major) is the safe fallback if a regression in the new line blocks you.

Run nvm install --lts to install the latest LTS, then nvm use --lts to activate it for the current shell, and nvm alias default 'lts/*' so future shells get the same version.

To remove old versions after upgrading: nvm uninstall 20.19.0. To list everything you have installed locally: nvm ls.

Use winget upgrade OpenJS.NodeJS.LTS, or download the latest LTS .msi from nodejs.org/en/download and run it. The installer replaces the previous version in place; global npm packages installed under the user prefix survive the upgrade. PATH entries are preserved.

For version switching (keeping multiple Node majors side-by-side), install fnm (winget install Schniz.fnm) or nvm-windows first, then use those to install and switch between versions.

Use fnm for a fresh setup. It is faster, cross-platform (Linux, macOS, Windows), and reads the same .nvmrc files as nvm so any existing project pinning still works. Shell startup is noticeably quicker because fnm is a single Rust binary, not a Bash script.

Stay with nvm if you have existing tooling, dotfiles, or team docs that depend on the nvm command syntax and you do not want to migrate. They are functionally equivalent for most workflows.

Three layered options, pick whichever fits your team:

  1. .nvmrc with a single line like 24 or lts/*. Read by nvm, fnm, and GitHub actions/setup-node.
  2. package.json engines, engines: { node: '>=22.0.0' }. Combine with engine-strict=true in .npmrc so installs fail on mismatch instead of just warning.
  3. Volta, volta pin node@24.10.0 writes a "volta" key into package.json and every developer in the repo transparently gets that exact version.

Native modules (anything with a .node binary, like bcrypt, sharp, better-sqlite3) are compiled against a specific Node ABI. When you upgrade Node majors, the ABI changes and the old binary refuses to load.

The fix is to rebuild: npm rebuild inside the project. If that fails, nuke and reinstall: rm -rf node_modules package-lock.json && npm install. Make sure build tools are present (build-essential on Linux, Xcode Command Line Tools on macOS, the Node MSI build-tools option on Windows).

Change the FROM line in your Dockerfile from, say, node:22-alpine to node:24-alpine, then rebuild with docker build --no-cache -t myapp:22 . Native modules are recompiled automatically during the npm install layer because the install runs against the new Node binary.

Always pin to a major or major.minor in production (node:24-alpine, node:24.10.0-alpine). Never use node:latest, it tracks Current, not LTS, and flips to the next major every October.

In GitHub Actions, use actions/setup-node@v7 with node-version-file: '.nvmrc' (or 'package.json' if the project uses Volta). The action reads the same pinning file your developers use locally, so CI and dev cannot drift.

For multi-version coverage, combine that with a matrix: matrix.node-version: [22.x, 24.x] and use node-version instead of node-version-file. Library authors should test against every LTS in active support.

Sources

Authoritative references this article was fact-checked against.

TagsNode.jsJavaScriptnvmfnmVoltaVersion ManagementCLIDockerGitHub ActionsDevOps

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Systems and Network Architect · Chief Technology Officer

Systems and network architect and Chief Technology Officer with more than two decades designing, building, and running production software, cloud and network architecture, Linux systems, and the bare metal underneath them, and lately working AI into the stack. A US Army veteran who served in Operation Iraqi Freedom. What I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Generate an SSH key with ssh-keygen: pick Ed25519 over RSA, decide on a passphrase, and set the ~/.ssh permissions SSH needs. Linux, macOS and Windows.

How to Create an SSH Key in 2026

Create an SSH key in one ssh-keygen command. Which key type to pick in 2026 (Ed25519 vs RSA), whether to set a passphrase, and the file permissions SSH needs, on Linux, macOS and Windows.

How to uninstall Node.js completely on Linux, macOS, and Windows, including removing leftover npm, npx, and global package directories

How to Uninstall Node.js (Every Install Method)

How to uninstall Node.js cleanly on Linux, macOS, and Windows: version managers (nvm, fnm, Volta, n), the official installer, apt/dnf/brew/winget/choco, Docker, plus verification and optional npm-cache or PATH cleanup.

Terminal output of node --version and npm --version showing the installed Node.js and npm release numbers on the command line

How to Check Your Node.js and npm Version

Run node --version and npm --version to see what you have installed. This covers every way to check Node and npm, finding which install is on PATH, reading the version inside a script, and the gotchas with version managers and multiple installs.