PyInstaller 打包 FastAPI + Uvicorn:启动文件写法与依赖收集

直接打包 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.pyif __name__ == "__main__": 保护,避免多进程递归启动。

生产环境建议

PyInstaller 打包适合桌面工具分发,生产服务器更推荐:

  • Docker 容器(环境固定、更新方便)
  • systemd + venv(直接部署,无需打包)
  • Supervisor 管理进程

打包后可执行文件体积通常在 50-150 MB,启动时间比直接运行慢 3-10 秒。