Node.js, Python and Ruby Selectors: Run Real Apps on CloudLinux

How the CloudLinux Node.js, Python and Ruby Selectors let shared hosting accounts run Express, Django or Rails apps through Passenger inside their own LVE.

Published
Reading time
5 min

Shared hosting was built for PHP: a request arrives, a script runs, the process ends. A Node.js or Python application is the opposite, a long-running process that listens on a port, and on a normal shared server there is no safe way to let a customer start one. CloudLinux's Node.js, Python and Ruby Selectors close that gap. They give each account a choice of interpreter versions and a wizard that starts the app under Passenger, inside the user's own LVE and CageFS, so it is contained like everything else the user runs.

What the selectors actually do

Each selector is three parts. The interpreters are alt-nodejs, alt-python and alt-ruby packages under /opt/alt/, several versions side by side, in the same way alt-php works for PHP Selector. The web server is Apache with mod_passenger, which starts the application on demand and proxies requests from a domain or a path on that domain to it. And the panel page, Setup Node.js App or Setup Python App, writes the Passenger configuration for the user so nobody edits Apache by hand.

Because the app process belongs to the user, it lives inside that user's LVE. Its memory counts against PMEM, its CPU against SPEED, and a runaway build step hits NPROC like any other process. CageFS keeps it from seeing the rest of the server.

Installing the interpreters

Install every version of a language with the group, or single versions with the selector tool:

bash
yum groupinstall alt-nodejs
yum groupinstall alt-python
yum groupinstall alt-ruby
cloudlinux-selector install-version --interpreter nodejs --version 22
cagefsctl --force-update

Then open Admin Level → Extra Features → CloudLinux Manager → Selector in DirectAdmin and switch on the Node.js, Python and Ruby tabs, choosing which versions users may pick. Today that is typically Node.js 22 and 24, Python 3.12 and 3.13, and Ruby 3.3. Unless a customer names an older version, do not offer it: end-of-life Node releases are not maintained by anyone.

Setup Node.js App

A DirectAdmin user opens Advanced Features → Setup Node.js App and clicks Create Application. The form asks for:

  • Node.js version from the enabled list.
  • Application mode: development or production. Production sets NODE_ENV and turns off verbose errors.
  • Application root: a directory relative to the home directory, for example myapp. Put the code here, not in public_html.
  • Application URL: the domain and an optional path. An app on / takes over the whole domain; an app on /api sits beside a normal website.
  • Application startup file: app.js, server.js, or whatever the entry point is.
  • Environment variables: DATABASE_URL, API keys and so on, added as key and value rows.

Click Create. Upload the code and package.json into the application root, come back to the page and click Run NPM Install. Passenger starts the app on the first request. Passenger intercepts the app's listen() call, so a plain app.listen(3000) works and the port number is ignored; do not bind to a specific IP address.

The page also shows a source command that puts the selected Node version in your shell, which is what to use in SSH for anything beyond what the buttons do.

Setup Python App

Python follows the same form with two extra fields. Application startup file defaults to passenger_wsgi.py, and Application Entry point to application, the WSGI callable Passenger imports. For Django the startup file is a few lines:

python
import os, sys
sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

The selector creates a virtualenv for the app under ~/virtualenv/<application root>/<version>/. Put a requirements.txt in the application root and click Run pip install; the page shows the exact source .../bin/activate line for working in that environment over SSH. Static files still need to be served by Apache, so point STATIC_ROOT at a directory under the domain's document root.

Ruby

Setup Ruby App works the same way. The app is a Rack application, so Passenger looks for config.ru in the application root, and gems come from a Gemfile installed with the page's bundle button. Rails works; a config.ru is generated with a new Rails project, and RAILS_ENV goes in the environment variables.

Restarting and reading errors

Passenger keeps the process alive between requests and starts it again after a code change only when told. Click Restart on the app's row, or from SSH:

bash
touch ~/myapp/tmp/restart.txt

Passenger restarts the app on the next request when that file's timestamp changes. When an app refuses to start, the reason is in stderr.log in the application root: a missing module means npm install or pip install was not run after the last upload, a syntax error means the startup file, and a connection refused to a database usually means the host should be localhost rather than the server's public address.

The same from the command line

Everything the wizard does has a CLI form, which is useful for provisioning scripts:

bash
cloudlinux-selector create --json --interpreter nodejs --user bob --app-root myapp --app-uri app --app-mode production --startup-file app.js --version 22
cloudlinux-selector restart --interpreter nodejs --user bob --app-root myapp
cloudlinux-selector stop --interpreter nodejs --user bob --app-root myapp

start, stop and restart take the same --user and --app-root pair.

Limits to set before customers arrive

A Node process sits at 100 MB or more of memory doing nothing, and npm install on a large project can briefly fork dozens of processes. The default LVE limits of 1 GB PMEM and 100 processes are enough for one modest app, but a package sold as "Node.js hosting" needs 2 GB and a higher NPROC, otherwise the first npm install fails with a fork error and the customer blames the platform. Set that at package level, as described in how to set LVE limits, and watch the memory column in CloudLinux Manager for the first week. When an app genuinely needs several gigabytes or its own worker processes, it has outgrown shared hosting, and a VPSPioneer managed VPS is the honest next step.

#cloudlinux#nodejs#python#ruby#passenger

Keep reading

More from CloudLinux

All guides

CloudLinux

CloudLinux PHP Selector: A PHP Version and Extensions Per Account

How CloudLinux PHP Selector gives each account its own PHP version, extensions and php.ini values, what it needs on DirectAdmin, and the CLI to manage it.

5 min read →

CloudLinux

What CloudLinux Does for Shared Hosting Stability (and Why It Matters)

How CloudLinux keeps one busy account from slowing a shared server: LVE limits, CageFS, PHP Selector, what a 508 error means and how to stay within limits.

6 min read →

CloudLinux

CageFS: What It Does and How to Enable It on CloudLinux

Why every CloudLinux server should run CageFS, what it hides from users, how to initialise and enable it, and how to fix the usual breakages.

4 min read →