When I was setting up the backend for my final year project, I decided to build it with FastAPI. Everything was running smoothly locally, but then came deployment time. Since I was using cPanel shared hosting, I ran straight into a brick wall.
cPanel’s Phusion Passenger server speaks WSGI (the synchronous standard built for Flask or Django). But FastAPI is natively ASGI (asynchronous, built for Uvicorn and high-concurrency event loops).
Out of the box, when Apache/Passenger tried to read my main.py, it didn't know what to do with a FastAPI app, and everything threw errors. I checked with popular AIs, but they kept giving me solutions that required root access, systemd, or running permanent background Uvicorn daemons—none of which you have on standard shared hosting.
After digging through old Reddit threads, Medium posts, and community forums, I figured out how to build a custom translation bridge. Here is how I solved it.
Step 1: Building the passenger_wsgi.py Bridge
change the cpanel_username & domain_name
venv_packages = "/home/cpanel_username/virtualenv/://domain_name.com"
import sys
import os
base_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, base_dir)
sys.path.insert(0, os.path.join(base_dir, "api"))
# venv_packages = "/home/cpanel_username/virtualenv/Backend_Folder_Name/3.11/lib/python3.11/site-packages"
venv_packages = "/home/cpanel_username/virtualenv/://domain_name.com"
if venv_packages not in sys.path:
sys.path.insert(1, venv_packages)
log_file = os.path.join(base_dir, "passenger_error.log")
def log_msg(msg):
try:
with open(log_file, "a", encoding='utf-8') as f:
import datetime
f.write(f"[{datetime.datetime.now()}] {msg}\n")
except:
pass
log_msg("=== PASSENGER WSGI STARTUP ===")
try:
from main import app # change this accordingly
log_msg("[OK] FastAPI app imported")
# Standard HTTP status reason phrases
HTTP_STATUS_PHRASES = {
200: "OK", 201: "Created", 204: "No Content",
301: "Moved Permanently", 302: "Found", 304: "Not Modified",
307: "Temporary Redirect", 308: "Permanent Redirect",
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden",
404: "Not Found", 405: "Method Not Allowed", 409: "Conflict",
422: "Unprocessable Entity", 429: "Too Many Requests",
500: "Internal Server Error", 502: "Bad Gateway",
503: "Service Unavailable",
}
def application(environ, start_response):
"""WSGI application wrapper for FastAPI ASGI app"""
import asyncio
method = environ['REQUEST_METHOD']
path = environ.get('PATH_INFO', '/')
log_msg(f"[REQUEST] {method} {path}")
try:
try:
content_length = int(environ.get('CONTENT_LENGTH') or 0)
except (ValueError, TypeError):
content_length = 0
if content_length > 0:
request_body = environ['wsgi.input'].read(content_length)
else:
request_body = b''
headers = []
ct = environ.get('CONTENT_TYPE')
if ct:
headers.append((b'content-type', ct.encode('latin-1')))
if content_length:
headers.append((b'content-length', str(content_length).encode('latin-1')))
# All other HTTP_* keys
for key, value in environ.items():
if key.startswith('HTTP_'):
header_name = key[5:].replace('_', '-').lower().encode('latin-1')
headers.append((header_name, value.encode('latin-1')))
scope = {
'type': 'http',
'asgi': {'version': '3.0'},
'http_version': '1.1',
'method': method,
'scheme': environ.get('wsgi.url_scheme', 'http'),
'path': path,
'query_string': environ.get('QUERY_STRING', '').encode(),
'root_path': environ.get('SCRIPT_NAME', ''),
'headers': headers,
'server': (environ.get('SERVER_NAME', 'localhost'), int(environ.get('SERVER_PORT', 80))),
'client': (environ.get('REMOTE_ADDR', '127.0.0.1'), int(environ.get('REMOTE_PORT', 0) or 0)),
}
# Variables to store response
resp_status = 200
resp_headers = []
body_parts = []
async def receive():
return {'type': 'http.request', 'body': request_body}
async def send(message):
nonlocal resp_status, resp_headers, body_parts
if message['type'] == 'http.response.start':
resp_status = message['status']
resp_headers = message.get('headers', [])
elif message['type'] == 'http.response.body':
body = message.get('body', b'')
if body:
body_parts.append(body)
async def run_app():
await app(scope, receive, send)
# Run the ASGI app
asyncio.run(run_app())
# Prepare headers for WSGI
wsgi_headers = [
(name.decode('latin-1') if isinstance(name, bytes) else name,
value.decode('latin-1') if isinstance(value, bytes) else value)
for name, value in resp_headers
]
# Build correct status line
reason = HTTP_STATUS_PHRASES.get(resp_status, "Unknown")
start_response(f"{resp_status} {reason}", wsgi_headers)
log_msg(f"[RESPONSE] {method} {path} - Status: {resp_status}")
# Return body parts
return body_parts if body_parts else [b'']
except Exception as e:
log_msg(f"[ERROR] {type(e).__name__}: {str(e)}")
import traceback
log_msg(traceback.format_exc())
start_response('500 Internal Server Error', [('Content-Type', 'text/plain')])
return [b'Internal Server Error']
log_msg("[OK] WSGI application ready")
except Exception as e:
log_msg(f"[CRITICAL] {type(e).__name__}: {str(e)}")
import traceback
log_msg(traceback.format_exc())
raise
Step 2: Paste this code inside htaccess
change all the variables with real names{Domain_Name, cpanel_username, backend_folder_name}
# DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION BEGIN
PassengerAppRoot "/home/cpanel_username/backend_folder_name"
PassengerBaseURI "/"
PassengerPython "/home/cpanel_username/virtualenv/backend_folder_name/3.11/bin/python"
# DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION END
# BEGIN cPanel-generated php ini directives, do not edit
<IfModule php8_module>
php_value error_log "/home/cpanel_username/logs/php.error.log"
php_flag log_errors On
</IfModule>
<IfModule lsapi_module>
php_value error_log "/home/cpanel_username/logs/php.error.log"
php_flag log_errors On
</IfModule>
# END cPanel-generated php ini directives, do not edit
# 1. Force HTTPS and handle non-www to www redirect
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Optional: If you want Apache to also force non-www to www
automatically at the API level
# change domain name
RewriteCond %{HTTP_HOST} ^Domain_Name\.com [NC]
RewriteRule ^(.*)$ https://www.Domain_Name.com/$1 [R=301,L]
# Pre-flight OPTIONS check
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ - [R=200,L]
</IfModule>
# 2. DYNAMICALLY match whichever URL is calling (with or without www)
<IfModule mod_headers.c>
SetEnvIf Origin "^https://(www\.)?Domain_Name\.com$" AccessControlAllowOrigin=$0
Header always set Access-Control-Allow-Origin "%{AccessControlAllowOrigin}e" env=AccessControlAllowOrigin
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, PUT, DELETE"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With, Accept"
Header always set Access-Control-Allow-Credentials "true"
</IfModule>
## Step 3: Redirect URL for CORS Error
How will it help you?
- Someone searches https://domain_name.com -> It will redirect you https://www.domain_name.com or vise versa.
- Mainly API CORS problem solution.
Enjoy Your FastAPI

Top comments (0)