Panellicense

Set up CloudLinux Node.js Selector on a cPanel server

Give shared-hosting customers their own Node.js runtime per app — version 18/20/22, isolated under CageFS, managed from cPanel's Setup Node.js App icon. End-to-end in about 25 minutes.

9 min readUpdated 2026-05-16cloudlinux · nodejs · selector · cpanel
schema: HowToschema: FAQPageschema: BreadcrumbList

Most shared cPanel servers still treat Node.js as a second-class citizen — customers either don't get it at all, or they get a single system-wide version that the sysadmin chose in 2022 and can never upgrade. CloudLinux's Node.js Selector solves this the same way PHP Selector solves the PHP version problem: each cPanel user picks their own runtime, the apps run isolated inside CageFS via Phusion Passenger, and Apache or LiteSpeed proxies the public URL to the right user-space process.

This guide is the end-to-end install — alt-nodejs packages, Passenger wiring, cPanel UI verification, and the per-app limits you should set before customers start deploying. It takes about 25 minutes on a 4-vCPU VPS.

Prerequisites

You need:

  • CloudLinux installed with an active licence (installing CloudLinux on cPanel if you haven't).
  • CageFS enabled and working — Node.js Selector runs apps inside the cage, so a broken cage means broken Node apps. The PHP Selector guide above covers the CageFS init sequence if you haven't done it.
  • Apache with mod_lsapi and mod_passenger, or LiteSpeed Web Server 5.4+. The default mod_ruid2 handler does not work — Passenger needs to spawn per-user processes via mod_passenger.
  • At least 4 GB free in /opt/alt/ per Node.js major version installed.
  • Bumped LVE memory limits — Node.js apps with npm install regularly need 1.5 GB+ of resident memory during dependency builds. A 512 MB PMEM limit will kill installs with cryptic npm ERR! killed messages.

Step 1 — Install the alt-nodejs package set

CloudLinux ships Node.js as separate alt-nodejs<MAJOR> packages, independent of any system or NodeSource Node.js you might already have:

yum install alt-nodejs18 alt-nodejs20 alt-nodejs22

The full LTS set takes 1.2-1.5 GB on disk. To install every supported version including end-of-life ones (useful when migrating in legacy apps):

yum groupinstall alt-nodejs

Confirm what landed:

cloudlinux-selector list --interpreter=nodejs --json | jq '.data.versions'

You should see one entry per installed major version, with the installed: true flag and a default boolean. Note which one is marked default — that's what new apps get if the user doesn't override.

Step 2 — Install and enable Passenger

Node.js Selector uses Phusion Passenger to spawn per-user processes and proxy requests from Apache. On CloudLinux's EasyApache 4 build this is a single profile change:

yum install ea-apache24-mod_alt_passenger

Then in WHM under EasyApache 4 → Customize → Apache Modules, tick mod_alt_passenger and rebuild. Or from the CLI:

ea_install_profile --install /etc/cpanel/ea4/profiles/cpanel/default.json

Verify Passenger is loaded:

httpd -M 2>/dev/null | grep passenger

Expected output: alt_passenger_module (shared). If empty, the rebuild didn't pick up the module — check /etc/cpanel/ea4/ea4.conf for the entry.

Step 3 — Enable Node.js Selector

The selector itself is one command:

cloudlinux-selector enable --interpreter=nodejs

This:

  • Registers the Setup Node.js App icon in cPanel under the Software section.
  • Mounts /opt/alt/alt-nodejs* paths read-only into every CageFS user.
  • Writes per-user shell wrappers so node, npm, and npx resolve to the user-selected version inside SSH.

Restart cpsrvd so the new icon appears immediately for logged-in users:

service cpsrvd restart

Step 4 — Set sensible defaults

The defaults file at /etc/cloudlinux-selector/nodejs.json controls what new apps get if the user doesn't pick a version:

nano /etc/cloudlinux-selector/nodejs.json

A working 2026 default:

{
  "default_version": "20",
  "allowed_versions": ["18", "20", "22"],
  "default_mode": "production",
  "default_passenger": {
    "max_pool_size": 4,
    "min_instances": 0,
    "pool_idle_time": 300
  }
}

min_instances: 0 is important on a shared box — it lets idle apps spin down after pool_idle_time seconds rather than holding RAM forever. The cost is a ~500 ms cold-start on the next request, which is usually fine for a low-traffic hobby site and very much not fine for a production e-commerce app. Customers who care can bump min_instances in their own app config.

Apply the defaults:

cloudlinux-selector apply-defaults --interpreter=nodejs

Step 5 — Confirm the cPanel UI works

Log in as a non-root cPanel user, scroll to Software, and click Setup Node.js App. You should see an empty app list and a Create Application button. Click through with these settings and create a smoke-test app:

  • Node.js version: 20
  • Application mode: Production
  • Application root: nodetest
  • Application URL: /nodetest
  • Application startup file: app.js

Click Create. The selector creates /home/<user>/nodetest/, a /home/<user>/nodevenv/nodetest/20/ virtualenv, and a Passenger config block in the user's vhost.

Drop a minimal app.js into the application root:

const http = require('http');
http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('node ' + process.version);
}).listen(process.env.PORT || 3000);

Restart the app from the UI (or cloudlinux-selector restart --interpreter=nodejs --app-root=/home/<user>/nodetest --user=<user>) and hit https://<user-domain>/nodetest — you should see node v20.x.x.

Step 6 — Set per-user limits before customers deploy

Two limit groups matter for Node.js workloads on shared hosting:

LVE memorynpm install of a typical React or Next.js project peaks at 1.2-1.8 GB. The default 1 GB PMEM limit will kill the install with no useful error in the user's terminal. Bump it cluster-wide, or per-user for known Node.js customers:

lvectl set <username> --pmem=2G --vmem=0

vmem=0 (unlimited virtual memory) is correct here — Node.js's V8 reserves huge virtual address spaces it never touches, and a non-zero vmem limit will cause apparently random out of memory errors during startup.

EP (entry processes) — each running Node.js app counts as 1 EP. Default is usually 20, which is fine. But every passenger-status poll, every npm run from SSH, and every cron job spawning node adds to the count. For users running 5+ Node apps, bump to 40.

See CloudLinux LVE tuning without angry customers for the full per-user resource model.

Three configuration mistakes that flood support

  1. Forgetting to rebuild Apache after installing mod_alt_passenger. The yum install completes, but until EasyApache 4 rebuilds the httpd config, no apps will start. They show "Started" in the cPanel UI but return 503 on the public URL. Always run /scripts/restartsrv_httpd after the rebuild.

  2. Setting Application URL to /. Mounting an app at the document root makes Passenger handle every request to the domain, including static files under public_html. This silently breaks WordPress, image galleries, and anything else under the same vhost. Use a subdirectory like /api or /app, or create a dedicated subdomain for the Node.js app and point the document root at the app's public folder.

  3. Letting users run npm install -g. Global installs land in /opt/alt/alt-nodejs*/root/usr/lib/node_modules/, which is read-only inside CageFS. The install appears to succeed in the user's shell but the package vanishes on next login. The selector ships a per-user wrapper that redirects globals to the user's virtualenv — make sure users run source /home/<user>/nodevenv/<app>/<ver>/bin/activate before any npm work, which the UI does automatically but SSH users won't.

A common gotcha — WebSocket and long-poll apps

Passenger proxies HTTP/1.1 cleanly, but WebSocket upgrades through Apache need an explicit mod_proxy_wstunnel rule. Add this to the user's .htaccess under their app root:

RewriteEngine On
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:%{ENV:PASSENGER_PORT}/$1" [P,L]

If you're on LiteSpeed instead of Apache, WebSocket works out of the box via LSWS's native proxy — no extra config needed. This is one of the few places where LSWS is meaningfully simpler than Apache for a Node-heavy shared environment, covered in LiteSpeed vs OpenLiteSpeed for hosts.

Logs to keep an eye on

tail -F /var/log/cloudlinux-selector.log
tail -F /usr/local/apache/logs/error_log | grep -i passenger
passenger-status                              # per-app process count and memory
cloudlinux-selector get-status --interpreter=nodejs --user=<user>

A healthy server shows Passenger spawning processes on request and reaping them after pool_idle_time. If you see passenger-status reporting dozens of processes per user with no traffic, min_instances got cranked up somewhere — check the user's app config and the global defaults.

Does Node.js Selector require CageFS?+
Yes. The selector mounts alt-nodejs paths into the user's cage and uses CageFS-aware Passenger to spawn processes as the right user. Without CageFS, `cloudlinux-selector enable --interpreter=nodejs` returns an error.
Can I run Node.js Selector without CloudLinux?+
No. The selector is a CloudLinux-only feature — it depends on CageFS, LVE, and the patched alt_passenger module. On stock CentOS/AlmaLinux you have to either run a single system-wide Node.js, use NodeSource RPMs, or move to a Node-friendly panel like CloudPanel.
What Node.js versions does CloudLinux support in 2026?+
Currently supported: 18 (LTS, EOL April 2025), 20 (LTS, EOL April 2026), and 22 (LTS, EOL April 2027). End-of-life versions stay in the alt-nodejs repos for migration purposes but stop receiving CloudLinux security backports. Check `yum list available 'alt-nodejs*'` for the current set.
Why do `npm install` commands hang or get killed on my CloudLinux server?+
Almost always an LVE memory limit. Default PMEM of 1 GB is below the peak working set of `npm install` for a modern React/Next.js app. Bump to 2 GB with `lvectl set <user> --pmem=2G --vmem=0` and retry. If installs still hang, check that `vmem` is set to 0 — non-zero vmem breaks V8's address-space reservation.
Can users SSH in and run `node` directly?+
Yes, once they source the app's virtualenv: `source /home/<user>/nodevenv/<app>/<ver>/bin/activate`. After that, `node`, `npm`, and `npx` resolve to the selected version. Without activation, the system PATH points to a stub that prints a help message.
How do I migrate an existing Passenger-on-NodeSource app to Node.js Selector?+
Create a new app in the cPanel UI with the same startup file and application root, copy the source over, run `npm install` from inside the activated virtualenv, then switch the public domain to point at the new app URL. There's no in-place migration — Node.js Selector apps live in a different process model than vanilla Passenger.

Next steps

Switch in an afternoon

Switch from your current reseller — free.

We migrate active cPanel, Plesk, LiteSpeed and CloudLinux licenses from any reseller. We prorate the first month so you never pay twice, and your customers see zero downtime during the swap.