Node.js moves fast and every project ends up tied to a version. An admin panel written two years ago runs on an old release, while a new Next.js project needs a current LTS. With a single system-wide Node.js from the package manager, keeping both on one server is painful. nvm, the Node Version Manager, lets each user keep several Node.js versions in their home directory and switch between them with one command.
How nvm works
nvm is a shell script that downloads Node.js builds into ~/.nvm. Switching only changes the PATH of the current session, so no root access is needed and users do not affect each other. The flip side is that an nvm-installed Node.js is visible only to that user and only in shells where nvm is loaded.
Installing nvm
Use the install script from the official GitHub repository; check the README for the latest release tag. The example uses v0.40.1.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm --version
The script appends loader lines to ~/.bashrc (or ~/.zshrc for zsh). If you get "command not found", close and reopen the terminal or run the source command again, since the current shell has not loaded nvm yet.
Installing and switching versions
For production pick LTS releases, which receive security fixes for longer.
nvm ls-remote --lts
nvm install --lts
nvm install 18
nvm use 18
nvm alias default 20
nvm ls
node -v
nvm use affects only the open terminal; new sessions load the default alias. Make the most-used version the default and switch to others as needed.
.nvmrc: the project remembers its version
Put the version number in a .nvmrc file in the project folder and run nvm use without arguments. Commit it to Git so the whole team uses the same version, and add an engines range to package.json for a warning on mismatches.
cd /var/www/eski-panel
echo "18" > .nvmrc
nvm install
nvm use
PM2, cron and systemd
This is where most problems appear. nvm loads only in interactive shells, and cron jobs or systemd services never read ~/.bashrc. Everything works in the terminal while the cron job fails with "node: command not found". Use the full path to the binary instead.
nvm which 20
# crontab
*/10 * * * * /home/deploy/.nvm/versions/node/v20.18.0/bin/node /var/www/misol-app/jobs/sync.js
The version in the path is an example; copy yours from nvm which. PM2 uses the Node.js version active when it was installed, and per-app versions can be pinned with the interpreter field.
apps: [{
name: "eski-panel",
script: "index.js",
interpreter: "/home/deploy/.nvm/versions/node/v18.20.4/bin/node"
}]
After upgrading Node.js you may need to rerun pm2 startup, because the startup script contains the old path. PM2 warns you when this happens.
Example: two projects on one VPS
A small web studio in Samarkand hosts two client projects on one VPS. The first is a 2022 Express panel for misol.uz that breaks above Node.js 18 because of old packages. The second is a new Next.js site for example.uz that needs Node.js 20 or newer. The studio installed nvm for the deploy user, added both versions, placed a .nvmrc in each project and set an interpreter path per app in the PM2 ecosystem file. Updating the old panel no longer touches the new site.
Cleaning up old versions
Unused versions pile up in ~/.nvm. Before removing one, make sure no PM2 app or cron job still points at it.
nvm uninstall 16
nvm cache clear
A VPS from the sayt.uz cabinet gives you a user account where nvm can be set up without touching system packages.