直接打包 uvicorn main:app 的方式容易出现模块找不到的问题,推荐单独写入口文件。
项目结构
project/
├── main.py # FastAPI app 定义
├── start.py # PyInstaller 打包入口
├── templates/ # Jinja2 模板(如有)
└── static/ # 静态文件(如有)
入口文件 start.py
关键点:直接传 app 对象,不用字符串
import uvicorn
from main import app
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
字符串形式 uvicorn.run("main:app", ...) 在 PyInstaller 环境中无法动态导入,会报:
Error loading ASGI app. Could not import module "main"
打包命令
基础打包:
pyinstaller -F start.py
FastAPI/Uvicorn 有大量动态导入,需要显式收集:
pyinstaller -F start.py \
--collect-all fastapi \
--collect-all uvicorn \
--collect-all starlette \
--collect-all pydantic
包含模板和静态文件(macOS/Linux 用 :,Windows 用 ;):
# Linux/macOS
pyinstaller -F start.py \
--collect-all fastapi \
--collect-all uvicorn \
--collect-all starlette \
--collect-all pydantic \
--add-data "templates:templates" \
--add-data "static:static"
# Windows
pyinstaller -F start.py ^
--collect-all fastapi ^
--collect-all uvicorn ^
--collect-all starlette ^
--collect-all pydantic ^
--add-data "templates;templates" ^
--add-data "static;static"
打包后静态文件路径
打包后程序解压到临时目录,__file__ 路径会变。读取打包内的文件需要:
import sys
import os
def get_resource_path(relative_path):
if getattr(sys, 'frozen', False):
base = sys._MEIPASS # PyInstaller 临时目录
else:
base = os.path.dirname(__file__)
return os.path.join(base, relative_path)
# 挂载静态文件
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory=get_resource_path("static")), name="static")
常见错误
ModuleNotFoundError: No module named ‘uvicorn.loops’
--collect-all uvicorn
pydantic validator 找不到
--collect-all pydantic
multiprocessing 相关报错
确保 start.py 有 if __name__ == "__main__": 保护,避免多进程递归启动。
生产环境建议
PyInstaller 打包适合桌面工具分发,生产服务器更推荐:
- Docker 容器(环境固定、更新方便)
- systemd + venv(直接部署,无需打包)
- Supervisor 管理进程
打包后可执行文件体积通常在 50-150 MB,启动时间比直接运行慢 3-10 秒。
