Deploy FastAPI on cPanel Shared Hosting (WSGI) Using an ASGI Bridge

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/myschola/virtualenv/scholarx_backend/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

Choose Your URL
Redirect Domain

How will it help you?

  1. Someone searches https://domain_name.com -> It will redirect you https://www.domain_name.com or vise versa.
  2. Mainly API CORS problem solution.

**

Enjoy Your FastAPI

**

Leave a Reply