Use nvm on a development machine, because you will need to switch versions. Use the NodeSource repository on a server, because it integrates with apt and unattended upgrades. Use plain apt install nodejs only if you genuinely do not care which version you get, since Ubuntu ships whatever was current when the release was frozen and that is frequently years old.
All three methods work and they suit different situations. The mistake that costs the most time is using nvm on a server, because processes that do not read your shell profile cannot find the binary, and that failure is deeply unhelpful when it happens.
Table of contents
- nvm, for development machines
- NodeSource, for servers
- Plain apt, and why the version is old
- The nvm-on-a-server mistake
- Choosing, and the version to pick
- How this fits the rest of the stack
- FAQ
nvm, for development machines
The Node Version Manager installs Node into your home directory and lets you switch between versions per project.
# Install nvm.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.6/install.sh | bash
# Load it without restarting the shell.
. "$HOME/.nvm/nvm.sh"
# Install and use a version.
nvm install 24
nvm install 22
nvm use 22
nvm alias default 22
node -v
npm -v
The reason this is right for development is that you will eventually have two projects wanting different versions, and without nvm that is a genuinely annoying problem.
# A project pins its version in .nvmrc:
echo "22" > .nvmrc
# Then, in that directory:
nvm use # reads .nvmrc
nvm install # installs it if missing
No sudo is needed for global npm installs, because everything lives under your home directory. That alone removes a category of permission problems people hit with system-wide installs.
Two caveats. nvm is a shell function rather than a binary, so anything that does not source your shell profile cannot see it. And each installed version has its own global packages, so switching versions appears to uninstall your global tools.
NodeSource, for servers
NodeSource maintains apt repositories for current Node versions, which gives you a system-wide install managed through the normal package tooling.
# Add the repository for the major version you want.
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
node -v
which node # /usr/bin/node
The absolute path is why this is the right choice on a server. The binary is at /usr/bin/node, which is on the default PATH for cron jobs, systemd units, and any process started by the system, with no profile sourcing required.
Updates come through apt like everything else, so security patches arrive with your normal update routine rather than needing a separate step someone has to remember.
Piping a script from the internet into bash deserves a moment’s thought. If that is not acceptable in your environment, add the repository manually, which is what the script does anyway.
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
| sudo tee /etc/apt/sources.list.d/nodesource.list
sudo apt-get update && sudo apt-get install -y nodejs
Note that npm comes with the NodeSource package. Running apt install npm separately pulls Ubuntu’s own version and its dependencies, which conflicts with what you just installed. Do not do it.
Plain apt, and why the version is old
sudo apt update
sudo apt install nodejs npm
node -v
Ubuntu freezes package versions at release and then only backports security fixes. So an LTS release from two years ago ships the Node that was current two years ago, for its entire supported life. That is a deliberate stability policy rather than neglect, and it is a poor fit for a runtime that releases a major version annually.
There is also a historical quirk worth knowing: on Debian and Ubuntu the binary was once named nodejs rather than node, because of a package name conflict. Current versions provide node, but old scripts and old advice still reference nodejs, and that is where the confusion comes from.
Use this method when the version genuinely does not matter, or when a policy requires everything to come from distribution repositories. Otherwise take NodeSource, which is the same delivery mechanism with a current version.
The nvm-on-a-server mistake
This is the single most common Node deployment problem on Ubuntu and it is worth understanding rather than just avoiding.
nvm works by modifying PATH in your shell profile. When you SSH in, the profile is sourced and node resolves. When cron, systemd, or a supervisor starts a process, the profile is not sourced, PATH is minimal, and node is either not found or resolves to an old system version.
The symptoms are distinctive: the application runs perfectly when you start it by hand and fails from the service manager, or a cron job reports a syntax error on modern JavaScript that runs fine interactively.
If you must use nvm on a server, use absolute paths everywhere non-interactive.
# Find the real path once.
which node
# /home/deploy/.nvm/versions/node/v22.11.0/bin/node
[Unit]
Description=my-app
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/home/deploy/app
EnvironmentFile=/home/deploy/app/.env
ExecStart=/home/deploy/.nvm/versions/node/v22.11.0/bin/node server.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
That path contains a specific version, so upgrading Node breaks the unit file silently. Which is the argument for NodeSource on servers restated as a concrete cost: the version-independent path at /usr/bin/node does not have this problem.
Choosing, and the version to pick
- Development machine: nvm, with .nvmrc in each project.
- Server running your application: NodeSource, for the system path and apt-managed updates.
- Container image: the official Node base image, which handles both concerns and is the correct answer for containers.
- Version pinned by policy to distribution packages only: plain apt, accepting whatever version that is.
On which version: take the current LTS, which is the even-numbered releases. They receive thirty months of support and are what libraries test against. Odd-numbered releases are for trying new features and are not supported long enough to build on.
Check End-of-Life status before deploying rather than after. Running a Node version past its support window means no security patches, and it is the sort of thing that goes unnoticed until an audit.
And pin the version in your project, with an engines field in package.json and an .nvmrc file, so the version is a property of the project rather than of whoever’s machine it was last run on.
How this fits the rest of the stack
Choosing an install method is a question you only have to answer when you are managing the server yourself. Deployed from a GitHub repository, the runtime version is part of the project configuration rather than something installed on a machine, and each deploy gets a build log, a live route, and a rollback to the previous version. The RunxBuild hosting calculator shows the Node service and its managed database as separate line items.
Useful related references:
- How to Install Docker on Ubuntu: 2026 Guide (22.04 and 24.04)
- Turn On SSH on Ubuntu: install, enable, and connect
- Self-Hosted GitLab: The Omnibus Install on Ubuntu
- Node services on RunxBuild
FAQ
Why does apt install an old version of Node.js?
Ubuntu freezes package versions at release and backports only security fixes, so an LTS release ships whatever Node was current at the time for its entire life. Use the NodeSource repository for a current version through the same apt tooling.
Should I use nvm or apt to install Node.js?
nvm on a development machine, because you will need to switch versions per project. NodeSource on a server, because it installs to /usr/bin/node which cron and systemd can find without sourcing a shell profile.
Why can cron not find node when nvm is installed?
nvm modifies PATH in your shell profile, and cron does not source it. Use the absolute path to the binary, or install through NodeSource so node is at a system path.
Do I need to install npm separately on Ubuntu?
No. npm ships with Node from both nvm and NodeSource. Running apt install npm pulls Ubuntu’s own version and conflicts with what you already installed.
Which Node.js version should I install?
The current LTS, which is an even-numbered release with thirty months of support. Odd-numbered releases are short-lived and not suitable for anything you have to maintain.