site logo

Marico's space

在 cPanel 共享主机上使用 ASGI Bridge 部署 FastAPI(WSGI)

前端技术 2026-10-29 17:33:51 2

最近折腾毕业设计后端,踩了几个坑,这篇把问题说清楚:在 cPanel 共享主机上部署 FastAPI,到底怎么绕过 Passenger 只认 WSGI 的限制。

本地开发时一切正常,FastAPI 跑在 Uvicorn 上,接口、异步任务和高并发都没问题。可到了部署环节,直接撞墙。

cPanel 使用的 Phusion Passenger 支持 WSGI(Web Server Gateway Interface,Web 服务器网关接口),这是 Flask、Django 这类同步框架常用的标准;FastAPI 原生使用 ASGI(Asynchronous Server Gateway Interface,异步服务器网关接口),主要面向 Uvicorn 和高并发异步事件循环。

默认情况下,Apache 和 Passenger 读取 main.py 后,并不知道该怎么调用 FastAPI 应用,结果就是各种报错。我问过几个主流 AI(人工智能)工具,得到的方案不是要求 root 权限,就是让我用 systemd 启动常驻 Uvicorn 进程。问题是,标准 cPanel 共享主机根本不给这些条件。

翻了一圈 Reddit、Medium 和技术社区的旧帖子后,我终于拼出了一套可行方案:在 WSGI 和 ASGI 之间加一层桥接。下面是完整做法。

步骤 1:编写 passenger_wsgi.py 桥接文件

把 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 

步骤 2:把下面配置粘贴到 htaccess 中

把 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>

步骤 3:配置域名跳转,解决 CORS 报错

选择你的 URL
Redirect Domain

这样配置有什么用?

  1. 有人访问 https://domain_name.com 时,会跳转到 https://www.domain_name.com;反过来也一样。
  2. 主要用于解决 API(应用程序接口)的 CORS(跨域资源共享)问题。

配置完成后,就可以在 cPanel 共享主机上通过 WSGI 调用 FastAPI 了。