Ultra Web Hosting runs Python web apps on shared hosting through Phusion Passenger, wired up by cPanel's Setup Python App tool. You pick a Python version, cPanel builds you a virtual environment, and Passenger serves your Flask or Django app the same way it serves everyone else's, no long-running process for you to babysit. This guide walks through creating the app, installing your packages, wiring the passenger_wsgi.py entry point, and getting both Flask and Django online, plus the restart, logging, and shared-hosting limits that catch people out.
- How Python Apps Run on Shared Hosting
- Create the App in cPanel
- Activate the Virtualenv and Install Packages
- The passenger_wsgi.py Entry Point
- A Minimal Flask App
- A Django App End to End
- Environment Variables and Secrets
- Restarting After Code Changes
- Logs and Troubleshooting
- Shared Hosting Limits and When to Move to a VPS
Passenger Serves It, cPanel Sets It Up
You never start a Gunicorn or uWSGI process by hand on shared hosting. cPanel's Setup Python App creates a virtualenv and hooks your app into Phusion Passenger. Passenger starts, stops, and scales your app on demand behind Apache.
- Pick a Python version and app root in cPanel
- Install packages into the virtualenv with pip over SSH
- Expose a WSGI callable named
applicationinpassenger_wsgi.py - Restart by touching
tmp/restart.txtor clicking Restart in cPanel
Code Changes Need a Restart
Passenger loads your app once and keeps it in memory. Editing a .py file does nothing visible until you restart the app. This is the single most common source of "my change did not take" tickets. Section 08 shows the two ways to restart.
- Symptom: you edited code, reloaded the page, nothing changed
- Cause: Passenger is still running the old copy in memory
- Fix:
touch tmp/restart.txtor click Restart in cPanel
01. How Python Apps Run on Shared Hosting
If you have deployed Python before, you are used to running your app under a WSGI server such as Gunicorn or uWSGI and keeping it alive with something like systemd or supervisor. On shared hosting you do not do any of that. Ultra Web Hosting uses Phusion Passenger, an application server built into our web stack, and cPanel's Setup Python App (also called Application Manager) is the front end that configures it for you.
The model is worth understanding before you touch anything:
You Run the Process
You launch Gunicorn, bind a port, put Nginx in front, and use a process manager to keep it running and restart it on boot.
- You manage the WSGI server and ports
- You own uptime, restarts, and logging
- Full control, full responsibility
Passenger Runs It for You
Passenger starts your app on the first request, feeds it traffic through Apache, and spins workers up or down. You just supply the code and the virtualenv.
- No port to bind, no server process to start
- Passenger handles start, stop, and scaling
- You expose one WSGI callable and restart on demand
Because Passenger loads your app on demand, there is no persistent process you control and no place to run a long-lived background worker. That constraint matters later, and Section 10 covers where the shared-hosting model runs out of room. For a Node equivalent of this same Passenger workflow, see article #487.
02. Create the App in cPanel
Everything starts in cPanel. This step creates the virtualenv and registers the app with Passenger.
- Log in to cPanel and open Setup Python App (under the Software section). Some themes label it Application Manager.
- Click Create Application.
- Python version: pick the version your project targets. Newer is usually better, but match what your code and dependencies expect (for example 3.11).
- Application root: the folder your app lives in, relative to your home directory, for example
myapp. cPanel creates this folder and puts the virtualenv alongside it. - Application URL: the domain or subdomain path where the app is served, for example
yourdomain.comoryourdomain.com/app. - Application startup file and Entry point: leave these at the defaults for now (
passenger_wsgi.pyandapplication). You will create that file shortly. - Click Create.
cPanel now builds a Python virtual environment for you and shows a command near the top of the app's page that looks like source /home/youruser/virtualenv/myapp/3.11/bin/activate && cd /home/youruser/myapp. Copy that line. You need it in the next section. For where these paths sit on the server and how Python is laid out, see article #127.
cPanel drops a starter passenger_wsgi.py and a public folder into the app root when it creates the app. You will replace the starter file with your own in Section 04.
03. Activate the Virtualenv and Install Packages
You install your dependencies from the command line, inside the virtualenv cPanel built. This needs SSH access. If you have not enabled SSH yet, see article #60 for how to turn it on and connect.
- Connect to your account over SSH.
- Paste the activation command cPanel gave you on the app page. It both activates the virtualenv and changes into the app root:
source /home/youruser/virtualenv/myapp/3.11/bin/activate && cd /home/youruser/myapp - Your shell prompt now shows the environment name in parentheses. Confirm you are using the right interpreter:
which python python --version - Install your packages with pip. For a single package:
pip install flask - For a real project, keep dependencies in a
requirements.txtin the app root and install them all at once:pip install -r requirements.txt
A minimal requirements.txt for a Flask app looks like this:
Flask==3.0.3
gunicorn==22.0.0
Always pin exact versions in requirements.txt with ==, as shown above, rather than leaving a bare Flask. Unpinned installs pull whatever is newest the day you run pip, so an app that worked last month can break on a redeploy when a dependency ships a breaking release. Pinning also means your local machine and the server run identical code. Generate a pinned file from a working environment with pip freeze > requirements.txt.
04. The passenger_wsgi.py Entry Point
Passenger looks for a file named passenger_wsgi.py in your app root, and inside it, a WSGI callable named application. That callable is the single contract between your code and the server. Whatever framework you use, the job of passenger_wsgi.py is to produce an object called application.
For a Flask app whose Flask instance is named app in a file called app.py, the entire passenger_wsgi.py is one line:
from app import app as application
That imports your Flask object and re-exposes it under the name Passenger expects. There is nothing else to do. No port, no host, no app.run() for production.
Passenger will not find your app if the callable is named anything other than application, unless you changed the Entry point field in cPanel to match. If you see a generic Passenger error page and the log says it could not find the WSGI application, this naming mismatch is the first thing to check.
05. A Minimal Flask App
Here is a complete, working Flask app. Two files in your app root: app.py holds the application, and passenger_wsgi.py hands it to Passenger.
app.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
return "Hello from Flask on Ultra Web Hosting"
@app.route("/health")
def health():
return {"status": "ok"}
if __name__ == "__main__":
# Only used for local development, never in production on Passenger.
app.run(debug=True)
passenger_wsgi.py
from app import app as application
- Put both files in your app root (for example
/home/youruser/myapp). - Make sure Flask is installed in the virtualenv (Section 03).
- Restart the app (Section 08) so Passenger loads the new code.
- Visit your Application URL. You should see the greeting, and
/healthshould return JSON.
The if __name__ == "__main__" block only runs when you execute python app.py yourself for local testing. Passenger imports the module and calls application directly, so that block is skipped in production.
06. A Django App End to End
Django ships with its own WSGI entry point, so wiring it into Passenger is a matter of pointing passenger_wsgi.py at it and doing the usual Django housekeeping. Assume your project is named myproject and lives in the app root.
Point passenger_wsgi.py at Django's WSGI app
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
Set ALLOWED_HOSTS and turn off debug
In myproject/settings.py, list the domain the app is served from and disable debug mode for production:
DEBUG = False
ALLOWED_HOSTS = ["yourdomain.com", "www.yourdomain.com"]
Leaving DEBUG = True on a live site exposes stack traces, settings, and environment details to anyone who triggers an error. Set it to False before the app is public, and read your real errors from the Passenger log instead (Section 09).
Run migrations and collect static files
Over SSH, with the virtualenv active and inside the app root:
python manage.py migrate
python manage.py collectstatic --noinput
In production Django does not serve static or media files itself. Point STATIC_ROOT at a folder the web server can reach and let Apache serve it. A common layout:
import os
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STATIC_URL = "/static/"
STATIC_ROOT = os.path.join(BASE_DIR, "public", "static")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "public", "media")
The public folder in the app root is the one Passenger treats as your document root, so anything under it is served as a plain static file. Run collectstatic again any time you change static assets, then restart the app.
07. Environment Variables and Secrets
Do not hard-code database passwords, API keys, or Django's SECRET_KEY into files you commit to a repository. cPanel's Setup Python App page has an environment variables section for exactly this: add a name and value there, save, and Passenger injects it into your app's environment.
- Open your app in Setup Python App.
- In the Environment variables area, click Add Variable.
- Enter a Name (for example
SECRET_KEYorDATABASE_URL) and its Value. - Save, then restart the app so the new value is loaded.
Read them in code the normal way:
import os
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///db.sqlite3")
Add any local .env file to .gitignore and keep the real values in the cPanel app config, not in the repository. That way a leaked repo does not leak your credentials, and each environment (local, staging, production) can hold its own values without a code change.
08. Restarting After Code Changes
Passenger loads your app into memory once and reuses it across requests. Editing a Python file, changing settings, or adding an environment variable has no effect until you tell Passenger to reload. There are two ways to do it.
Touch the restart file
Passenger watches for a file at tmp/restart.txt in your app root. Updating its timestamp triggers a graceful reload on the next request:
mkdir -p tmp
touch tmp/restart.txt
Click Restart in cPanel
On the Setup Python App page, each application has a Restart button. Click it and Passenger reloads the app. This is the same effect as touching the restart file, just from the browser.
If you deploy new code and the site still shows the old behavior, you almost certainly forgot this step. Restart the app, then reload the page. When in doubt after any change to code, settings, dependencies, or environment variables, restart.
09. Logs and Troubleshooting
When something breaks, Passenger writes the Python traceback to the app's stderr log. That log is where you find the real cause behind a generic error page. Its location is shown on the Setup Python App page for your app, and it is usually under the app root or in your home directory as stderr.log. Watch it live over SSH while you reproduce the error:
tail -f /home/youruser/myapp/stderr.log
Common problems and what they mean:
- 500 Internal Server Error with a generic page. Your app raised an exception on startup or during the request. Read the traceback in the stderr log; it names the file and line.
- ModuleNotFoundError or ImportError. The package is not installed in the virtualenv, or you installed it into a different environment. Re-activate the correct virtualenv (Section 03) and
pip installit, then restart. - Wrong Python version. If a dependency refuses to install or behaves oddly, confirm
python --versioninside the activated virtualenv matches what you selected in cPanel. Changing the version in cPanel rebuilds the environment, so reinstall your packages after. - Static files return 404 (Django). You have not run
collectstatic, orSTATIC_ROOTdoes not point inside thepublicfolder Passenger serves. Fix the path, runcollectstatic --noinput, and restart. - Changes not taking effect. You did not restart. See Section 08.
If you need a task to run on a schedule (clearing sessions, sending digests, pruning data), do not try to keep a background loop running inside the app. Use a scheduled cron job that activates the virtualenv and runs a management command. See article #479 for setting up cron jobs.
10. Shared Hosting Limits and When to Move to a VPS
Passenger on shared hosting is a great fit for websites, dashboards, APIs, and internal tools with normal request-and-response traffic. There are things the shared model is not built for, and pushing against them leads to frustration rather than a fix.
Consider a VPS or dedicated server when your app needs any of the following:
- Long-running background workers. Celery workers, queue consumers, or any daemon that must stay alive independent of web requests. Passenger only runs your app while it is serving traffic, so there is nowhere to keep a persistent worker.
- WebSockets or long-lived connections. Real-time features that hold a socket open per client do not sit well behind the shared Apache and Passenger setup.
- High or spiky memory use. Shared plans cap per-account memory and processes. A model-loading data app or a large in-memory cache will hit those ceilings.
- System-level access. Some Python packages shell out to system binaries. On shared hosting, functions like
system()andexec()are restricted for security, which affects certain libraries. Article #235 covers those shared-hosting execution limits; they apply to Python subprocess calls too.
None of this means you have to leave. Plenty of production Flask and Django sites run happily on our shared plans. But if your roadmap includes real-time features, heavy background processing, or full root control, a VPS gives you your own Gunicorn, systemd, and the run of the box. We can help you size and migrate when you get there.
Stuck Getting Your App Online?
If Passenger is throwing errors you cannot decode, or you are weighing whether your project has outgrown shared hosting, open a ticket. Send us your app root, the Python version, and the stderr log output, and we will point you at the fix or the right plan.
Open a Support TicketQuick Recap: Python on Passenger in Six Steps
If you only remember six things from this guide, remember these:
- Create the app in cPanel with Setup Python App: choose the Python version, app root, and URL, and let it build the virtualenv.
- Activate the virtualenv over SSH using the command cPanel gives you, then
pip install -r requirements.txt. - Expose a callable named application in
passenger_wsgi.py(one line for Flask, Django'sget_wsgi_application()for Django). - For Django, set ALLOWED_HOSTS, DEBUG = False, run
migrateandcollectstatic, and point static and media into thepublicfolder. - Put secrets in the cPanel app config as environment variables, never in the repository.
- Restart after every change with
touch tmp/restart.txtor the Restart button, and read errors from the stderr log.
Last updated July 2026 · Browse all Getting Started articles
