Node.js 22 LTS on Ubuntu 24.04 on Azure User Guide
Overview
Node.js 22 is the active LTS line ("Jod"), supported through April 2027, and remains the default JavaScript runtime for production server-side workloads. The cloudimg image installs Node.js 22.22.2 from the official NodeSource APT repository (deb.nodesource.com/node_22.x nodistro main), pinned with apt-mark hold so an unattended apt-get upgrade cannot drift the major version. Three companion tools come pre-installed at the global level: npm 10.9.7 (bundled with Node), pnpm 10.33.3 (a fast, disk-efficient alternative package manager) and pm2 7.0.1 (the de facto Node.js process manager). A working Express 4.21 + EJS sample application lives at /opt/nodeapp/cloudimg, run under a dedicated nodeapp system user by pm2-runtime through the cloudimg-app.service systemd unit, fronted by nginx as a reverse proxy. Per-VM secrets are generated at first boot.
What is included:
- Node.js 22.22.2 LTS (MIT) from NodeSource APT, supported through 2027-04-30
- npm 10.9.7 (bundled with Node), pnpm 10.33.3, pm2 7.0.1 — all installed globally
- Express 4.21 sample app at
/opt/nodeapp/cloudimg, owned by thenodeappsystem user - Express + EJS templating + helmet (security headers) + morgan (request logging) + ws (WebSocket)
- App binds to
127.0.0.1:3000; nginx reverse-proxies port 80 to it - nginx serves
/static/directly from/opt/nodeapp/cloudimg/public/ - Per-VM
SESSION_SECRET(32 hex) andADMIN_API_KEY(32 hex) rotated at first boot - Three systemd units:
nodejs-firstboot.service(oneshot),cloudimg-app.service(pm2-runtime), and the standardnginx.service - 24/7 cloudimg support
Prerequisites
Active Azure subscription, SSH key, VNet + subnet. Standard_B2s (4 GB RAM) is comfortable for a single-instance Express app under pm2. NSG inbound: allow 22/tcp from your management CIDR and 80/tcp from any client CIDR that needs the Node.js app.
Step 1-3: Deploy + SSH (standard pattern)
ssh azureuser@<vm-ip>
Step 4: Service Status + Versions
sudo systemctl is-active cloudimg-app.service nginx.service nodejs-firstboot.service
node --version
npm --version
pnpm --version
pm2 --version

Step 5: Read Per-VM Credentials
sudo cat /stage/scripts/nodejs-credentials.log
The credentials log holds the per-VM SESSION_SECRET (used by Express to sign session cookies) and the per-VM NODEAPP_ADMIN_API_KEY (passed via the X-Admin-Key header to reach /api/admin). The same values are mirrored into /etc/nodeapp/cloudimg.env, which is read by the cloudimg-app.service systemd unit at start-up.
Step 6: HTTP Health + JSON API
curl -sf -o /dev/null -w 'homepage: HTTP %{http_code}\n' http://127.0.0.1/
curl -sf http://127.0.0.1/api/info | python3 -m json.tool | head -10
The first call hits the Express homepage through nginx. The second confirms the JSON API is alive and reports the same nodeVersion you saw in Step 4.

Step 7: Browse the Sample App
Open http://<vm-ip>/ in a browser. The Express + EJS homepage renders the running Node version, the npm version, the request URL, the user agent, and a small live block driven by the /ws WebSocket echo endpoint.

Step 8: JSON API in Browser
Visit http://<vm-ip>/api/info — Chrome / Firefox will pretty-print the JSON. The same response is what your front-end SPA, mobile client, or a downstream service would consume programmatically.

Step 9: Admin API with Key
/api/admin is gated by the X-Admin-Key header. Visiting it without the key in a plain browser returns HTTP 401 Unauthorized with a JSON error body — that proves the key check is wired correctly.

To call it successfully from your terminal:
KEY=$(sudo grep '^NODEAPP_ADMIN_API_KEY=' /stage/scripts/nodejs-credentials.log | cut -d= -f2-)
curl -sf -H "X-Admin-Key: ${KEY}" http://127.0.0.1/api/admin | python3 -m json.tool
Step 10: pm2 Process Status
cloudimg-app.service runs the app via pm2-runtime so the systemd unit is the supervisor. To list the running pm2 processes from the nodeapp user perspective:
sudo -u nodeapp pm2 list
sudo journalctl -u cloudimg-app.service --no-pager -n 20
The pm2 list output shows a single cloudimg-app process with its CPU, memory and restart counters. The journal tail shows the last twenty Express request log lines from morgan.
Step 11: Install Additional npm Packages
To add a runtime dependency to the cloudimg sample app (replace <package> with the npm package you need):
cd /opt/nodeapp/cloudimg
sudo -u nodeapp npm install <package>
sudo systemctl restart cloudimg-app.service
The install must run as the nodeapp user so node_modules/ keeps the right ownership. The systemd restart triggers pm2-runtime to re-spawn the worker against the updated module tree.
Step 12: Tune pm2 Workers
The cloudimg sample app ships in fork mode (one worker). To run cluster mode across all CPU cores edit /opt/nodeapp/cloudimg/ecosystem.config.js and set the instances field on the cloudimg-app block:
instances: "max",
exec_mode: "cluster",
Then restart:
sudo systemctl restart cloudimg-app.service
sudo -u nodeapp pm2 list
instances: "max" tells pm2 to spawn one worker per CPU core (2 on Standard_B2s, 4 on Standard_B4s_v2, etc.). Use a number like instances: 2 if you want to leave headroom for nginx and the OS.
Step 13: Add Your Domain
Edit /etc/nginx/sites-available/nodeapp-cloudimg, set the server_name to your hostname, and reload nginx:
sudo sed -i 's/server_name _;/server_name apps.example.com;/' /etc/nginx/sites-available/nodeapp-cloudimg
sudo nginx -t
sudo systemctl reload nginx
nginx -t is a config-syntax dry-run; reload swaps configs without dropping live connections.
Step 14: Install Let's Encrypt SSL
Point your domain DNS A record at this VM, then:
sudo apt-get install -y python3-certbot-nginx
sudo certbot --nginx -d apps.example.com -m you@example.com --agree-tos --non-interactive --redirect
Certbot edits /etc/nginx/sites-available/nodeapp-cloudimg to add the SSL server block plus a :80 → :443 redirect, then reloads nginx. Renewal is handled by the certbot.timer systemd unit (runs twice daily).
Step 15: Update Node.js or Switch to nvm
Node.js 22 is held at the major version with apt-mark hold nodejs so an unattended apt-get upgrade never crosses a major boundary. To take a 22.x patch upgrade explicitly:
sudo apt-mark unhold nodejs
sudo apt-get update
sudo apt-get install -y --only-upgrade nodejs
sudo apt-mark hold nodejs
sudo systemctl restart cloudimg-app.service
If you need to run multiple Node versions side-by-side (for example a legacy Node 18 service and a new Node 22 service on the same VM), install nvm per-user and let each developer pin their own runtime:
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
. ~/.bashrc
nvm install 22
nvm use 22
The system-wide NodeSource Node 22 stays in place for cloudimg-app.service.
Step 16: Logs and Troubleshooting
sudo journalctl -u cloudimg-app.service --no-pager -n 80
sudo journalctl -u nginx.service --no-pager -n 30
sudo tail -50 /var/log/nginx/access.log
sudo tail -50 /var/log/nginx/error.log
sudo -u nodeapp pm2 logs cloudimg-app --lines 50 --nostream
The Express app logs all requests through morgan to stdout, so journalctl -u cloudimg-app.service is the canonical event log. nginx has its own access and error logs at /var/log/nginx/. pm2 also keeps per-process logs under /var/lib/nodeapp/.pm2/logs/ (visible via pm2 logs).
Security
- Per-VM
SESSION_SECRET(32 hex chars, used by Express to sign session cookies) - Per-VM
ADMIN_API_KEY(32 hex chars, required onX-Admin-Keyheader for/api/admin) NODE_ENV=productionso Express disables verbose error pages and enables view cachinghelmetmiddleware enabled — setsX-Frame-Options,X-Content-Type-Options,Strict-Transport-Security, CSP, etc.- App binds to
127.0.0.1:3000only — never directly reachable from the public network - nginx reverse-proxies port 80 to
127.0.0.1:3000(single ingress point, easy to put behind WAF/CDN) - NSG inbound restricted to
22/tcp(management) and80/tcp(clients) by default python3-certbot-nginxavailable for free Let's Encrypt TLS (Step 14)nodeappis a dedicated system user with no login shell — the app cannot escalate to root
Support
cloudimg provides 24/7/365 expert technical support. Guaranteed response within 24 hours, one hour average for critical issues. Contact support@cloudimg.co.uk.