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_lsapiandmod_passenger, or LiteSpeed Web Server 5.4+. The defaultmod_ruid2handler does not work — Passenger needs to spawn per-user processes viamod_passenger. - At least 4 GB free in
/opt/alt/per Node.js major version installed. - Bumped LVE memory limits — Node.js apps with
npm installregularly need 1.5 GB+ of resident memory during dependency builds. A 512 MBPMEMlimit will kill installs with crypticnpm ERR! killedmessages.
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, andnpxresolve 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 memory — npm 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
-
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_httpdafter the rebuild. -
Setting Application URL to
/. Mounting an app at the document root makes Passenger handle every request to the domain, including static files underpublic_html. This silently breaks WordPress, image galleries, and anything else under the same vhost. Use a subdirectory like/apior/app, or create a dedicated subdomain for the Node.js app and point the document root at the app'spublicfolder. -
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 runsource /home/<user>/nodevenv/<app>/<ver>/bin/activatebefore anynpmwork, 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?+
Can I run Node.js Selector without CloudLinux?+
What Node.js versions does CloudLinux support in 2026?+
Why do `npm install` commands hang or get killed on my CloudLinux server?+
Can users SSH in and run `node` directly?+
How do I migrate an existing Passenger-on-NodeSource app to Node.js Selector?+
Next steps
- Tune memory and process limits per user with CloudLinux LVE tuning without angry customers — Node workloads stress LVE differently than PHP.
- If you also offer PHP, the equivalent setup for PHP versions is in PHP Selector on cPanel — the two selectors share CageFS and the cPanel UI but otherwise run independently.
- Activate or renew a CloudLinux license before rolling Node.js Selector out to billable customers.