Python is the awkward middle child of shared cPanel hosting. Most sysadmins either ban it outright ("just use a VPS"), or expose a single system-wide interpreter that breaks the next time CloudLinux pushes a kernel update. CloudLinux's Python Selector solves this the same way PHP Selector and Node.js Selector solve their respective version problems: each cPanel user picks their own runtime, the WSGI app runs isolated inside CageFS via Phusion Passenger, and Apache or LiteSpeed proxies the public URL to the right user-space process.
This guide walks the full install — alt-python packages, Passenger wiring, the cPanel Setup Python App UI, and the per-user limits you need before customers start pushing Django projects. Budget about 30 minutes on a 4-vCPU VPS, most of it the EasyApache 4 rebuild.
Prerequisites
You need:
- CloudLinux installed with an active license. If you're starting from a stock AlmaLinux or CentOS 7 box, follow installing CloudLinux on cPanel first.
- CageFS enabled and initialised. Python Selector mounts alt-python paths into
each user's cage — without CageFS,
cloudlinux-selector enable --interpreter=pythonreturns an error and the cPanel icon never appears. - Apache with
mod_lsapiandmod_alt_passenger, or LiteSpeed Web Server 5.4+. The defaultmod_ruid2handler does not work for Python — Passenger needs to spawn per-user WSGI processes thatruid2can't manage. - At least 800 MB free in
/opt/alt/per Python minor version installed. - LVE memory limits bumped to at least 1.5 GB
PMEM. Django'scollectstaticand FastAPI's reload-on-change watcher both routinely peak above 1 GB during startup, and a tight limit kills them with no useful error in the user's terminal.
Step 1 — Install the alt-python package set
CloudLinux ships every supported Python as a separate alt-python<MAJOR><MINOR>
package, independent of the system Python that cPanel itself depends on:
yum install alt-python39 alt-python310 alt-python311 alt-python312
The full LTS-and-current set takes about 1.6 GB on disk. If you want every version including older ones for customers migrating in legacy Django 3.x apps, use the group:
yum groupinstall alt-python
Confirm what landed and which version is the default:
cloudlinux-selector list --interpreter=python --json | jq '.data.versions'
You should see one entry per installed minor version with installed: true and
exactly one with default: true. That default is what new apps get when the
user clicks Create Application without changing the dropdown — pick a
version your customers will actually want before going live.
Step 2 — Install and enable Passenger
Python Selector uses the same patched Phusion Passenger as Node.js Selector, so if you've already set up Node.js apps this step is already done — skip to step 3. Otherwise:
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 mod_alt_passenger
entry and rebuild again.
Step 3 — Enable Python Selector
One command:
cloudlinux-selector enable --interpreter=python
This:
- Registers the Setup Python App icon in cPanel under the Software section.
- Mounts
/opt/alt/alt-python*paths read-only into every CageFS user. - Writes per-user shell wrappers so
python,python3,pip, andvirtualenvresolve to the user-selected version inside SSH.
Restart cpsrvd so the new icon appears immediately:
service cpsrvd restart
Step 4 — Set sensible defaults
The defaults file at /etc/cloudlinux-selector/python.json controls what new
apps get when the user doesn't override:
nano /etc/cloudlinux-selector/python.json
A working 2026 default:
{
"default_version": "3.11",
"allowed_versions": ["3.9", "3.10", "3.11", "3.12"],
"default_mode": "production",
"default_passenger": {
"max_pool_size": 4,
"min_instances": 0,
"pool_idle_time": 300
}
}
min_instances: 0 matters on a shared box — it lets idle Django and Flask
apps spin down after five minutes of no traffic rather than holding 80-120 MB
of RAM each forever. The cost is a 1-2 second cold start on the next request,
which is fine for a hobby site and unacceptable for a high-traffic API.
Customers running production workloads can bump min_instances in their own
app config.
Apply the defaults:
cloudlinux-selector apply-defaults --interpreter=python
Step 5 — Confirm the cPanel UI works
Log in as a non-root cPanel user, scroll to Software, and click Setup Python App. You should see an empty app list and a Create Application button. Click through with:
- Python version: 3.11
- Application mode: Production
- Application root:
pytest - Application URL:
/pytest - Application startup file:
passenger_wsgi.py - Application entry point:
application
Click Create. The selector creates /home/<user>/pytest/, a virtualenv
under /home/<user>/virtualenv/pytest/3.11/, and a Passenger config block in
the user's vhost.
Drop a minimal passenger_wsgi.py into the application root:
import sys
def application(environ, start_response):
start_response('200 OK', [('Content-Type', 'text/plain')])
return [f'python {sys.version}'.encode()]
Restart the app from the UI (or run cloudlinux-selector restart --interpreter=python --app-root=/home/<user>/pytest --user=<user> as root)
and hit https://<user-domain>/pytest — you should see python 3.11.x.
Step 6 — Set per-user limits before customers deploy
Three limit groups matter for Python workloads on shared hosting:
LVE memory — Django's manage.py collectstatic peaks around 1 GB on a
medium app; a Pandas-heavy data tool can easily hit 2 GB. The default 1 GB
PMEM will OOM-kill these at startup with Killed and nothing in the
Passenger log. Bump per-user for known Python customers, or cluster-wide:
lvectl set <username> --pmem=2G --vmem=0
vmem=0 (unlimited) is correct — CPython's allocator reserves large virtual
ranges it never commits, and a non-zero vmem causes apparent random
MemoryError exceptions during ML library imports.
EP (entry processes) — each running Python app counts as 1 EP, plus one per Passenger worker spawned during a request burst. Default 20 is usually fine for a user running 1-2 apps. Customers running 5+ apps or any FastAPI with high concurrency should be at 40.
I/O — pip install of a heavy dependency tree (NumPy, SciPy, Pandas all
together) can read 200-300 MB of wheels from the cache and write the same
volume into the virtualenv. The default 1 MB/s IO limit turns a 90-second
install into 15 minutes. Bump to at least 10 MB/s for users you know will
manage real dependencies:
lvectl set <username> --io=10240 --iops=1024
See CloudLinux LVE tuning without angry customers for the full resource model.
Three configuration mistakes that flood support
-
Forgetting to rebuild Apache after installing
mod_alt_passenger. Theyum installcompletes silently, but until EasyApache 4 rebuilds the httpd config no apps will start. The cPanel UI shows the app as "Started" and the public URL returns 503. Always finish with/scripts/restartsrv_httpdafter the rebuild. -
Setting Application URL to
/. Mounting a Python app at the document root makes Passenger handle every request to the domain, including static files the user expects Apache to serve frompublic_html. This silently breaks any WordPress install or media gallery on the same vhost. Use a subdirectory like/apior/app, or use a dedicated subdomain with the document root pointing at the app'spublicfolder. -
Letting users run
pip install --userorsudo pip install. Both land outside the virtualenv —--userwrites to~/.local/, which the selector ignores on app spawn, andsudois blocked by CageFS anyway. Packages installed this way appear to work in the user's SSH session and thenModuleNotFoundErrorat request time. The activation script the cPanel UI runs setsPIP_USER=0andPIP_REQUIRE_VIRTUALENV=1, so SSH users hitting this should be told to activate first.
A common gotcha — Django and ALLOWED_HOSTS
A Django app deployed through Python Selector receives requests from
Passenger with the upstream Host header preserved, but the WSGIPath Apache
sees is the proxy URL, not the public domain. Django's default
ALLOWED_HOSTS = [] rejects every request with DisallowedHost and the user
sees a generic 500. The fix is to populate ALLOWED_HOSTS with the actual
public domain plus localhost:
ALLOWED_HOSTS = [
'example.com',
'www.example.com',
'localhost',
'127.0.0.1',
]
If you're moving customers in from cPanel's old EasyApache 3 mod_wsgi setup,
note that Passenger does not pass mod_wsgi's wsgi.input semantics — file
uploads larger than 100 KB need DATA_UPLOAD_MAX_MEMORY_SIZE raised in Django
settings, not just at the web-server level.
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=python --user=<user>
A healthy server shows Passenger spawning Python processes on request and
reaping them after pool_idle_time. If passenger-status shows dozens of
processes per user during quiet hours, min_instances was bumped — check the
user's app config in /home/<user>/<app>/passenger_wsgi.py and the global
defaults file.
Does Python Selector require CageFS?+
Can I run Python Selector without CloudLinux?+
What Python versions does CloudLinux support in 2026?+
Why does `pip install` fail or hang on my CloudLinux server?+
Can users SSH in and run `python` directly?+
How do I migrate an existing mod_wsgi or Gunicorn app to Python Selector?+
Next steps
- Tune memory, IO, and process limits per user with CloudLinux LVE tuning without angry customers — Python's resource profile sits between PHP and Node.js, closer to Node.
- If you also offer Node.js, the parallel guide is Node.js Selector on cPanel — same Passenger, different interpreter, shared CageFS.
- Activate or renew a CloudLinux license before rolling Python Selector out to billable customers.