Applications Azure

Node.js 22 LTS on Ubuntu 24.04 on Azure User Guide

| Product: Node.js 22.22.2 LTS on Ubuntu 24.04 LTS on Azure

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 the nodeapp system 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) and ADMIN_API_KEY (32 hex) rotated at first boot
  • Three systemd units: nodejs-firstboot.service (oneshot), cloudimg-app.service (pm2-runtime), and the standard nginx.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

All three services active and Node 22.22.2 + npm 10.9.7 + pnpm + pm2 reported

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.

Express homepage HTTP 200 and /api/info JSON with Node version

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.

Express + EJS sample app homepage in browser

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.

Browser view of /api/info returning JSON with nodeVersion and uptime

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.

Admin API endpoint returning 401 without X-Admin-Key header

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 on X-Admin-Key header for /api/admin)
  • NODE_ENV=production so Express disables verbose error pages and enables view caching
  • helmet middleware enabled — sets X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security, CSP, etc.
  • App binds to 127.0.0.1:3000 only — 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) and 80/tcp (clients) by default
  • python3-certbot-nginx available for free Let's Encrypt TLS (Step 14)
  • nodeapp is 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.