The nodejs package in Ubuntu’s own archive is the wrong Node.js for most projects. On 26.04 it installs Node 22 with npm 9.2, a release of npm that predates Node 22 itself, and on a fresh server it dragged in 568 packages and 738 MB because the archive’s npm is split into hundreds of separate Debian packages. On 24.04 the archive version is Node 18, which went end-of-life in April 2025.
This guide shows you how to install Node.js on Ubuntu and Debian from NodeSource, which ships the current LTS as a single package with a matching npm, and with nvm when you need several versions side by side. It then builds a native module, runs a small Express app under systemd and moves the install to Node.js 26. We ran every step on Ubuntu 26.04 in October 2026 and repeated the NodeSource install on Ubuntu 24.04 and Debian 13, with Node.js 24.22.0 and 26.12.0.
Pick a Node.js line
Production code belongs on an LTS line, because those get 30 months of security fixes. October 2026 is a changeover month, so there are two LTS lines right now and a third about to join them:
| Line | Status in October 2026 | Security fixes until | Best for |
|---|---|---|---|
| Node.js 26 | Current, becomes LTS on 28 October 2026 | 30 April 2029 | New projects that start now |
| Node.js 24 | Active LTS, moves to maintenance on 20 October 2026 | 30 April 2028 | Existing apps, the safe default today |
| Node.js 22 | Maintenance LTS | 30 April 2027 | Apps that cannot move yet |
Dates come from the Node.js release schedule. Install Node.js 24 for anything already in production, and start new projects on 26, because it will be supported a full year longer. Node 26 is also the last line under the old twice-a-year model: from Node 27 the project ships one major a year, in April, and every release becomes LTS that October, so the odd/even rule no longer applies. If you only want one specific line, the Node.js 24 on Ubuntu and Node.js 24 on Debian guides go deeper on that release.
The distro packages lag well behind all of this:
- Ubuntu 26.04 archive: Node 22 with npm 9.2
- Ubuntu 24.04 archive: Node 18, out of support
- Debian 13 archive: Node 20, out of support
Those packages exist for other Debian packages that depend on Node, not for application developers. They are also heavy, because the archive splits npm into hundreds of small packages: a simulated apt-get install nodejs npm wanted 540 new packages on 24.04 and 554 on Debian 13. Skip them.
What you need
Ubuntu server images ship everything this guide uses. A minimal Debian 13 install may not, because it skips sudo when you set a root password, has no curl, and only provides vi. Check that the machine has:
- Ubuntu 26.04, Ubuntu 24.04 or Debian 13 on amd64
- a user with sudo rights
curland an editor (we usevim)- about 250 MB free for Node.js, plus 230 MB per extra nvm version
On Debian, install the missing tools as root first with apt install -y sudo curl vim, then add your user to the sudo group with usermod -aG sudo josphat (use your own username) and log in again so the group takes effect.
Install Node.js from NodeSource
NodeSource publishes one apt repository per Node major line, plus a setup script that adds it. Download the script first and read it, rather than piping it straight into a root shell, because it runs as root and edits your apt configuration.
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
less nodesource_setup.sh
The script runs apt update, installs curl, gnupg, ca-certificates and apt-transport-https, and deletes any older NodeSource repository and key. Then it writes three things you will want to know about later:
/usr/share/keyrings/nodesource.gpg, the signing key/etc/apt/sources.list.d/nodesource.sources, the repository/etc/apt/preferences.d/nodejsandnsolid, two apt pins
Run it once you’re happy with what it does:
sudo -E bash nodesource_setup.sh
The script finishes by suggesting apt install nodejs. Before you run that, look at the repository and the nodejs pin, because those two files decide which build you get from now on.
cat /etc/apt/sources.list.d/nodesource.sources /etc/apt/preferences.d/nodejs
The repository uses the suite nodistro, so the same tree serves every Ubuntu and Debian release and you never wait for NodeSource to add a new codename. The pin gives NodeSource’s nodejs a priority of 600 over the archive’s 500, so apt picks NodeSource’s build even when the archive’s version string sorts higher. That matters more than it sounds: with setup_22.x on 26.04, Ubuntu’s 22.22.1+dfsg+~cs22.19.15-1ubuntu1 sorts above NodeSource’s 22.22.1-1nodesource1, and without the pin apt would install Ubuntu’s package.
Types: deb
URIs: https://deb.nodesource.com/node_24.x
Suites: nodistro
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/nodesource.gpg
Package: nodejs
Pin: origin deb.nodesource.com
Pin-Priority: 600
Now install it. NodeSource bundles npm and npx inside the nodejs package, so leave npm out of the command. We use apt-get in the commands whose output we show, because on 26.04 the newer apt prints its summary in a different layout; both install the same package.
sudo apt-get install -y nodejs
That was one package and 246 MB on 26.04, 24.04 and Debian 13 alike, against 568 packages for the archive’s nodejs and npm on 26.04. Check the versions next.
node -v
npm -v
npx -v
Node 24 ships npm 11, so the tooling matches the runtime and you do not need to upgrade npm separately.
v24.22.0
11.19.1
11.19.1
For Node.js 26 instead, use setup_26.x in the first command and nothing else changes. The repository path is the only thing that differs between lines, which is also how the upgrade later in this guide works.
nodejs : Conflicts: npm
Installing Ubuntu’s npm package on top of NodeSource’s nodejs fails, because NodeSource’s package declares a conflict with it. The error looks like an impossible dependency problem.
The following packages have unmet dependencies:
nodejs : Conflicts: npm
Nothing is broken. You already have npm, since it ships inside nodejs, so run npm -v to confirm it and drop npm from whatever script or Dockerfile tried to install it. Provisioning scripts written for the distro package hit this most often, because there the two really are separate packages.
Install build tools for native modules
Packages such as bcrypt, sharp and better-sqlite3 carry C++ code that npm compiles when no prebuilt binary matches your platform. Install a compiler and make up front, so those installs do not fail halfway through a deploy on a server that has no build tools.
sudo apt install -y build-essential
Recent npm versions also warn about dependencies that run install scripts. We saw this when installing bcrypt into a test project on both 26.04 and 24.04.
npm warn install-scripts [email protected] (install: node-gyp-build)
npm warn install-scripts
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.
On npm 11 this is only a warning: the module still installed and hashed a password correctly. npm 12, already out, goes further. According to GitHub’s npm 12 changelog, it stops running dependency preinstall, install and postinstall scripts by default, including the implicit node-gyp rebuild for packages with a binding.gyp, until you approve them. Approve the packages you trust while you’re still on npm 11, so native modules keep building when your build server moves to npm 12.
Install global npm packages
Global installs go to /usr/lib/node_modules, which belongs to root, so a plain npm install -g as your user fails with a permission error.
npm error code EACCES
npm error syscall rename
npm error path /usr/lib/node_modules/pm2
npm error dest /usr/lib/node_modules/.pm2-zkn4vkCL
npm error errno -13
npm error Error: EACCES: permission denied, rename '/usr/lib/node_modules/pm2' -> '/usr/lib/node_modules/.pm2-zkn4vkCL'
With a NodeSource install, use sudo for global tools that everyone on the machine should share, such as the pm2 process manager. Our Uptime Kuma with pm2 guide shows it running a real service.
sudo npm install -g pm2
pm2 --version
Do not fix the error by changing the ownership of /usr/lib/node_modules. The next nodejs upgrade writes into that directory as root again, and you end up with a mix of owners that breaks both apt and npm. If you want per-user global packages with no sudo at all, the nvm section below does exactly that.
Run a Node.js app with systemd
A quick Express app proves the runtime, npm and the network stack all work together. Create a project and install Express into it.
mkdir -p ~/hello && cd ~/hello
npm init -y
npm install express
Create the app file in the project directory.
vim ~/hello/app.js
It answers on port 3000 and reports the Node version that served the request, which is handy after an upgrade:
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello from Node ' + process.version + '\n'));
app.listen(3000, () => console.log('listening on :3000'));
Running it with node app.js works until you log out, because the process belongs to your SSH session. To keep it up across reboots and crashes, give it a systemd unit.
sudo vim /etc/systemd/system/hello-node.service
Change User and WorkingDirectory to your own account. ExecStart uses the full path to the NodeSource binary, because systemd does not load nvm or your shell profile, so a bare node could resolve to nothing at boot. Restart=on-failure brings the app back after a crash, which is most of what pm2 would give you here:
[Unit]
Description=Hello Node.js app
After=network.target
[Service]
User=josphat
WorkingDirectory=/home/josphat/hello
ExecStart=/usr/bin/node app.js
Restart=on-failure
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target
Start it, give Node a couple of seconds to bind the port, and send it a request. Without the pause, curl -s can hit the port before the app listens and print nothing at all.
sudo systemctl daemon-reload
sudo systemctl enable --now hello-node
sleep 2
systemctl is-active hello-node
curl -s http://127.0.0.1:3000/
The service reports active, and the reply carries the runtime version, which on our 24.04 box was the NodeSource build.
active
Hello from Node v24.22.0
If other machines must reach the app directly, open the port with sudo ufw allow 3000/tcp (Debian needs sudo apt install -y ufw first). For anything public, keep port 3000 closed and put nginx in front instead, so TLS, request limits and access logs live in one place and Node never faces the internet directly.
Run several versions with nvm
nvm installs Node.js under your home directory, one directory per version, and switches between them per shell. It suits a developer machine that juggles projects on different lines, and it can coexist with a NodeSource install. The longer guide to managing Node.js versions with nvm covers aliases, migrating global packages and per-project setup. Fetch the latest nvm release tag first:
NVM_VER=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep '"tag_name"' | cut -d '"' -f 4)
echo $NVM_VER
The echo should print a tag such as v0.40.8. If it prints nothing, the GitHub API rate limit kicked in, so wait a minute and repeat it. The installer command below only runs when the variable is set, because an empty tag produces a redirect page that curl would pipe straight into bash:
[ -n "$NVM_VER" ] && curl -so- "https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VER}/install.sh" | bash
The installer appends three lines to ~/.bashrc. Load them in the current shell, then install the newest LTS and Node 26 side by side.
source ~/.bashrc
nvm install --lts
nvm install 26
Each install downloads the official binary tarball and checks its SHA-256 before unpacking. Whatever version you install first becomes the default, so here new shells start on the LTS.
Computing checksum with sha256sum
Checksums matched!
Now using node v24.22.0 (npm v11.19.1)
Creating default alias: default -> lts/* (-> v24.22.0 *)
Pin a project to a line with an .nvmrc file in its root. Running nvm use inside that directory then picks the right version, so nobody on the team has to remember which project needs which Node. The mkdir -p is harmless if you already created ~/hello in the systemd section.
mkdir -p ~/hello && cd ~/hello
echo 26 > .nvmrc
nvm use
node -v
nvm reads the file and switches the current shell.
Found '/home/josphat/hello/.nvmrc' with version <26>
Now using node v26.12.0 (npm v11.20.0)
v26.12.0
Two things trip people up here. First, nvm only exists in your interactive shell, so sudo node, cron jobs and systemd units never see it: they resolve /usr/bin/node, the NodeSource build, whatever nvm use says in your terminal. Second, each version costs real disk, 208 MB for Node 24 and 230 MB for Node 26, so remove old ones with nvm uninstall. Use nvm for development, and NodeSource for anything a service runs.
Upgrade to Node.js 26
Node.js 26 becomes the Active LTS on 28 October 2026, and Node 24 drops to maintenance on 20 October. Moving a NodeSource install across major lines is a repository switch followed by an ordinary upgrade, so you don’t need to uninstall anything first.
curl -fsSL https://deb.nodesource.com/setup_26.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt-get install -y nodejs
Apt unpacks the new major over the old one in place.
1 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.
Need to get 42.3 MB of archives.
After this operation, 44.6 MB of additional disk space will be used.
Unpacking nodejs (26.12.0-1nodesource1) over (24.22.0-1nodesource1) ...
Global packages survive the upgrade, since they live in /usr/lib/node_modules outside the package, and pm2 still worked straight after. The runtime under your code has changed, though, so three things need attention after the jump:
- Rebuild native modules in each project with
npm rebuild, because a module compiled against one Node ABI can refuse to load on the next - Restart every Node service, since running processes keep the old binary until they restart
- Fix any
corepackcalls in scripts and CI, because Node 26 no longer ships it
The third one breaks builds that relied on corepack enable to get Yarn or pnpm, and it fails loudly rather than subtly. On Node 24 the command still printed a version, but after the upgrade our non-interactive test shell could not find it, so any CI step that calls it stops the pipeline:
bash: line 1: corepack: command not found
Corepack itself is still maintained, just no longer bundled, so the corepack project tells you to install it from npm with sudo npm install -g corepack, which keeps corepack enable and the packageManager field in package.json working as before. If you would rather drop it, install the package manager directly with sudo npm install -g pnpm or sudo npm install -g yarn; a pnpm and Turborepo monorepo needs the same change in its CI image. Our bcrypt test module loaded on Node 26 even before the rebuild, because it ships prebuilt Node-API binaries, but older modules that compile against V8 directly do not, so run the rebuild anyway.

The capture above is the whole move on our 26.04 test box: the repository points at node_26.x, apt unpacks 26 over 24, and the Express app answers from the new runtime after a restart.
Remove Node.js
Stop and remove the test service first, so systemd isn’t left with a unit that fails at every boot once /usr/bin/node is gone:
sudo systemctl disable --now hello-node
sudo rm /etc/systemd/system/hello-node.service
sudo systemctl daemon-reload
Then purge the package and delete the repository, both pins and the keyring the setup script added. Leaving the pin and repository behind is the trap, because a later apt install nodejs would quietly pull NodeSource’s build again instead of the one you expect.
sudo apt-get purge -y nodejs
sudo rm -f /etc/apt/sources.list.d/nodesource.sources /etc/apt/preferences.d/nodejs /etc/apt/preferences.d/nsolid /usr/share/keyrings/nodesource.gpg
sudo rm -rf /usr/lib/node_modules
sudo apt update
Purging Node 26 freed 290 MB and warned that /usr/lib/node_modules was not empty, because globally installed tools such as pm2 are not part of the package. That’s why the third command deletes the directory. Afterwards apt-cache policy nodejs falls back to the archive candidate, which confirms the NodeSource repository is gone. For nvm, delete ~/.nvm and remove the three NVM_DIR lines from ~/.bashrc.