Install Node.js 22 on Ubuntu Server Without a Desktop
Put Node.js 22 on a headless Ubuntu 24.04 VPS with the NodeSource repo, skip snaps and desktops, and verify a production-ready runtime in minutes.
Netbay Engineering
Netbay Engineering
On this page
A laptop with a desktop session is a fine place to write JavaScript. It is a terrible template for a production VPS. Ubuntu Server has no display manager, no software center, and no reason to install one just to get Node.js 22 on the PATH. The job is simpler: a signed apt repository, a single node binary under /usr/bin, and a system user that never opens a shell. That is the whole runtime. Everything else in this series assumes you have that shape and nothing fancier.
This walkthrough targets Ubuntu 24.04 on a Lucknow VPS. The same commands work on any headless Ubuntu host, including a Netbay instance that boots in under a minute. You will add the NodeSource 22.x repo, install the packages, prove that systemd can find the binary without a login profile, and skip the three install paths that look convenient and then break at boot.
Skip snaps, nvm, and a desktop stack
Ubuntu Desktop pulls in a display manager, a session bus, and a browser you will never sit in front of. On a 2 GB VPS that is RAM you needed for the event loop and for nginx. Snap node is a different trap: the binary lives under /snap, confinement fights common systemd settings, and refreshes land on a cadence you do not pick. nvm is a shell function aimed at developers hopping between versions in a terminal. It does not exist in a non-interactive ExecStart unless you wrap every command in bash -lc, which is how services vanish after the first reboot.
The production shape is boring on purpose. One apt-managed node. One unprivileged user. One PATH that systemd already has.
Add the NodeSource 22.x repository
Ubuntu universe ships a nodejs package, but it lags the 22 LTS line you actually want to run. NodeSource publishes a signed apt repo that tracks that LTS. Install the signing key into /etc/apt/keyrings, then drop the list file.
sudo apt-get update
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
node -v
npm -v
which nodenode -v should print v22 followed by a patch. npm arrives with the same package, so do not apt-get install npm on the side or you will mix two trees. which node should print /usr/bin/node. That path is what a unit file will call. If apt offers to replace the deb with a snap, refuse; you just configured a real repository and you want those files on disk.
Pin the package while you are here so a later unattended upgrade does not jump you onto 23 without a decision. apt-mark hold nodejs is the explicit version of hope.
Create a user systemd can actually run
Running Node as root turns a prototype bug into a host compromise. Create a system user with a home under /srv, no login shell, and no sudo. Confirm the same /usr/bin/node is visible to that user without sourcing a profile.
sudo useradd --system --create-home --home-dir /srv/app --shell /usr/sbin/nologin nodeapp
sudo -u nodeapp -- /usr/bin/node -v
sudo -u nodeapp -- /usr/bin/which node
printf '%s\n' 'const http = require("http"); http.createServer(function (req, res) { res.end("ok\n"); }).listen(3000, "127.0.0.1");' | sudo tee /srv/app/server.js
sudo chown nodeapp:nodeapp /srv/app/server.js
sudo -u nodeapp -- /usr/bin/node /srv/app/server.jsThe last line binds 127.0.0.1:3000. Leave it there only long enough to curl it from the same host, then stop the process. You have not opened a public port and you should not. A later post puts nginx in front. For now the test is that node starts as nodeapp, finds the script, and does not need a desktop session to do it.
Native addons are the one extra you might need. If a dependency compiles a C++ binding, install build-essential and python3 before npm ci, then remove the compiler toolchain after the build if you want a thinner attack surface. Do not leave gcc sitting on a public box because one package used node-gyp six months ago.
Keep the runtime current without a GUI
On a headless host, updates are apt and unattended-upgrades, not a software store. Review NodeSource changelog notes before you lift a hold. Do not wget a tarball into /usr/local and forget it. Tarball installs skip the package database, so dpkg -l cannot tell you what the box is running, and the next person on call will reinstall from memory.
If a native module needs a compiler, install it for the build, then apt-get purge build-essential when npm ci is done. A compiler toolchain is not part of a Node runtime. Corepack can enable pnpm or yarn if the repo asks for them; still keep the node binary itself on apt so the supervisor path stays /usr/bin/node.
The takeaway: Node on Ubuntu Server is a package, a user, and a loopback bind. Skip the desktop, skip snap, skip nvm as a service manager. Once /usr/bin/node runs as nodeapp, you are ready to wrap it in a unit file.
You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and follow along — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance