App Deployment·7 min read·

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.

NB

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.

bash
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 node

node -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.

bash
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.js

The 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.

Headless Node 22: the path that survives reboot NodeSource 22.x signed apt repo /usr/bin/node visible to systemd nodeapp nologin user 127.0.0.1 no public bind Avoid: Ubuntu Desktop Avoid: snap node Avoid: nvm in ExecStart apt binary plus a system user is the whole runtime Lucknow VPS, High-Speed SSD, no GUI required

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