# 从零部署 FastAPI 到宝塔生产环境:一份完整的踩坑记录

> 环境:阿里云 ECS + 宝塔面板 + Python 3.12 + FastAPI + MySQL + Nginx + HTTPS
>
> 目标:将一个 FastAPI 项目以生产级标准部署上线,并接入 MySQL 与 JWT 认证

## 一、背景

最近在做一个社交圈子类的 App,后端选了 FastAPI。部署环境是阿里云 ECS,使用宝塔面板管理。原以为「装个 Python、跑个 gunicorn」就完事,结果从 uv 到 gunicorn、从 Alembic 到 bcrypt,把能踩的坑几乎踩了个遍。

这篇文章完整记录了部署全过程遇到的问题和解决方案,希望能帮到后来人。

---

## 二、最终部署架构

```
用户浏览器 / App
↓ HTTPS (443)
Nginx 反向代理 (api.example.com)
↓ HTTP (127.0.0.1:8000)
gunicorn + uvicorn worker
↓
FastAPI + SQLAlchemy(async)
↓
MySQL (utf8mb4)
```

关键组件:
- **Python**:`/www/python/cpython-3.12.15-linux-x86_64-gnu/`
- **虚拟环境**:`/www/wwwroot/myapi/.venv`
- **进程管理**:gunicorn + uvicorn worker
- **表结构管理**:Alembic
- **认证**:JWT(python-jose + passlib)

---

## 三、问题清单(按出现顺序)

### 问题 1:`nohup: failed to run command 'uv': No such file or directory`

**现象**:用 `nohup uv ...` 后台启动,报找不到 `uv`。

**原因**:`uv` 装在 `/root/.local/bin/uv`,但当前 shell 的 `PATH` 里没有这个目录。

**解决方案**:

```bash
# 方法一:用绝对路径
nohup /root/.local/bin/uv run ... &

# 方法二:加到 PATH
export PATH="/root/.local/bin:$PATH"
nohup uv run ... &

# 永久生效
echo 'export PATH="/root/.local/bin:$PATH"' >> ~/.bashrc
```

---

### 问题 2:`uv add fastapi` 报「self-dependencies are not permitted」

**现象**:

```
error: Requirement name `fastapi` matches project name `fastapi`,
but self-dependencies are not permitted
```

**原因**:项目的 `pyproject.toml` 里 `name = "fastapi"`,与要安装的依赖重名,uv 以为你在让项目依赖自己。

**解决方案**:改项目名。

```toml
# pyproject.toml
[project]
name = "my-fastapi-app" # 不再叫 fastapi
```

---

### 问题 3:把 FastAPI 源码当成自己的项目

**现象**:项目路径是 `/www/wwwroot/fastapi/fastapi-master`,是从 GitHub 下载的 FastAPI 源码包。

**原因**:FastAPI 是**库**,不是应用模板。它没有 `main.py` 入口,无法 `fastapi dev` 启动。

**解决方案**:新建一个干净的目录,自己写 `main.py`:

```bash
mkdir -p /www/wwwroot/myapi && cd /www/wwwroot/myapi
uv init --name myapi
uv add "fastapi[standard]"
```

```python
# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
return {"hello": "world"}
```

---

### 问题 4:gunicorn 不存在 / 拼写错误

**现象**:`nohup: failed to run command '.../gunicorn': No such file or directory`

**原因**:
- 忘记装 gunicorn(`fastapi[standard]` 不含 gunicorn)
- 或者拼成了 `gunicor`(少一个 n)

**解决方案**:

```bash
cd /www/wwwroot/myapi
uv add gunicorn
# 或
.venv/bin/pip install gunicorn

# 验证
ls .venv/bin/gunicorn
```

---

### 问题 5:`Permission denied` 执行 gunicorn

**现象**:`ls -l` 显示 `-rwxr-xr-x`,属主 `www`,但 `sudo -u www ... gunicorn` 仍然 `Permission denied`。

**真正原因**:uv 默认把 Python 装在 `/root/.local/share/uv/python/...`,`gunicorn` 的 shebang 指向这个 Python,`www` 用户**没有权限访问 `/root`**,所以即使 gunicorn 本身可执行也无法启动。

**解决方案**:把 Python 装到共享目录,再用它重建 venv。

```bash
# 1. 用 uv 装 Python 到共享目录
export UV_PYTHON_INSTALL_DIR=/www/python
uv python install 3.12
chmod -R a+rX /www/python

# 2. 用这个 Python 重建 venv
cd /www/wwwroot/myapi
rm -rf .venv
/www/python/cpython-3.12.15-linux-x86_64-gnu/bin/python3.12 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install "fastapi[standard]" gunicorn

# 3. 修权限
chown -R www:www /www/wwwroot/myapi
chmod +x .venv/bin/*

# 4. 验证
sudo -u www /www/wwwroot/myapi/.venv/bin/gunicorn --version
```

---

### 问题 6:系统 Python 3.6 太老,uvicorn worker 加载失败

**现象**:

```
Error: class uri 'uvicorn.workers.UvicornWorker' invalid or not found
...
ModuleNotFoundError: No module named 'uvicorn'
```

**原因**:`python3 -m venv .venv` 用的是系统 Python 3.6,现代 FastAPI / uvicorn 要求 Python ≥ 3.8。

**解决方案**:用高版本 Python 重建 venv(同问题 5)。

```bash
# 确认系统 python3 版本
python3 -V # 若是 3.6,必须换
```

---

### 问题 7:宝塔 Python 项目管理器 502 / 启动失败

**现象**:宝塔 Python 项目状态异常,日志报 `ModuleNotFoundError`。

**原因**:
1. 宝塔会用**它自己另建的环境**运行,与你手动创建的 `.venv` 无关
2. 启动命令里写 `nohup ... &` 会干扰宝塔的进程守护

**解决方案**:
- **启动命令必须用绝对路径指向你自己的 venv**:

```
/www/wwwroot/myapi/.venv/bin/gunicorn main:app \
--workers 2 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000 \
--timeout 120 \
--forwarded-allow-ips='127.0.0.1'
```

- **不要写 `nohup`、不要 `&`、不要重定向**,宝塔会自己守护进程

---

### 问题 8:`Table 'users' already exists` 导致重启崩溃

**现象**:第二次启动时应用崩溃,日志报 `Table 'users' already exists`。

**原因**:`main.py` 的 `lifespan` 里调用了 `Base.metadata.create_all`,每次启动都尝试建表,表已存在就报错。

**解决方案**:去掉 `create_all`,用 Alembic 管理表结构。

```python
# main.py
@asynccontextmanager
async def lifespan(app: FastAPI):
# 表结构由 Alembic 管理
yield
await engine.dispose()
```

---

### 问题 9:Alembic 报 `Can't load plugin: sqlalchemy.dialects:driver`

**现象**:

```
sqlalchemy.exc.NoSuchModuleError: Can't load plugin: sqlalchemy.dialects:driver
```

**原因**:`alembic.ini` 里的默认 URL 是占位符 `driver://user:pass@localhost/dbname`,`env.py` 里的 `config.set_main_option` **位置写错了**(写进了 `run_migrations_offline`,而实际走的是 online 模式)。

**解决方案**:把 URL 设置放到**模块级别**。

```python
# alembic/env.py
from alembic import context
from database import Base, DATABASE_URL
from models.user import User # noqa: F401

config = context.config

# 关键:模块级别设置,online/offline 都生效
config.set_main_option("sqlalchemy.url", DATABASE_URL)

target_metadata = Base.metadata
```

---

### 问题 10:`ModuleNotFoundError: greenlet`

**现象**:

```
ImportError: The SQLAlchemy asyncio module requires that
the Python 'greenlet' library is installed.
```

**原因**:`pip install sqlalchemy` **不带**异步依赖,需显式安装 extras。

**解决方案**:

```bash
.venv/bin/pip install "sqlalchemy[asyncio]"
# 或
.venv/bin/pip install greenlet
```

> **教训**:装带 extras 的包**必须加引号**,否则 shell 会把方括号当通配符。

---

### 问题 11:`ModuleNotFoundError: jose`

**现象**:`from jose import JWTError` 报找不到。

**原因**:用**系统 pip** 装的包,没装进项目 `.venv`。

**解决方案**:永远用项目 venv 的 pip。

```bash
cd /www/wwwroot/myapi
.venv/bin/pip install "python-jose[cryptography]"
```

**验证**:

```bash
.venv/bin/python -c "import jose; print('ok')"
```

---

### 问题 12:MySQL `Access denied for user 'api'@'localhost'`

**现象**:

```
sqlalchemy.exc.OperationalError: (1044, "Access denied for user
'api'@'localhost' to database 'fastapi_app'")
```

**原因**:`api` 用户只对 `api` 库有权限,对 `fastapi_app` 库没有。

**排查**:

```bash
mysql -uroot -p -e "SHOW GRANTS FOR 'api'@'127.0.0.1';"
```

输出:

```
GRANT ALL PRIVILEGES ON `api`.* TO 'api'@'127.0.0.1' ← 只授权了 api 库
```

**解决方案**:

```sql
GRANT ALL PRIVILEGES ON `fastapi_app`.* TO 'api'@'127.0.0.1';
GRANT ALL PRIVILEGES ON `fastapi_app`.* TO 'api'@'localhost';
FLUSH PRIVILEGES;
```

**验证**:

```bash
mysql -uapi -p'密码' -h127.0.0.1 fastapi_app -e "SELECT 1;"
```

---

### 问题 13:`ValueError: Password cannot be longer than 72 bytes`

**现象**:注册接口 500,日志显示:

```
ValueError: Password cannot be longer than 72 bytes,
truncate manually if necessary
```

**原因**:`passlib 1.7.4` 与 `bcrypt 4.1+` 不兼容。passlib 初始化时会用一个超长测试密码探测 bcrypt 的历史 bug,bcrypt 4.1 不再容忍超过 72 字节的输入。

**解决方案**:降级 bcrypt。

```bash
.venv/bin/pip install "bcrypt==4.0.1"
```

**验证**:

```bash
.venv/bin/python -c "import bcrypt; print(bcrypt.__version__)" # 4.0.1
```

---

### 问题 14:502 Bad Gateway

**排查思路**:

```bash
# 1. 后端是否在跑
ss -tlnp | grep 8000
curl http://127.0.0.1:8000/api/v1/health

# 2. Nginx 错误日志
tail -50 /www/wwwlogs/api.example.com.error.log

# 3. 常见错误
# connect() failed (111: Connection refused) → 后端没起来
```

**最常见原因**:宝塔 Python 项目用的不是你的 `.venv`,里面缺依赖。

---

## 四、项目结构(最终版)

```
/www/wwwroot/myapi/
├── main.py
├── database.py
├── .env
├── requirements.txt
├── core/
│ ├── config.py # pydantic-settings 读 .env
│ └── security.py # JWT + bcrypt
├── models/
│ ├── __init__.py # from models.user import User
│ └── user.py
├── schemas/
│ └── auth.py
├── api/
│ ├── deps.py # get_current_user 依赖
│ └── v1/
│ ├── router.py
│ └── endpoints/
│ └── auth.py
└── alembic/
```

**关键代码片段**:

```python
# core/security.py
from datetime import datetime, timedelta
from jose import jwt
from passlib.context import CryptContext
from core.config import get_settings

settings = get_settings()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
return pwd_context.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
return pwd_context.verify(plain, hashed)

def create_access_token(subject: str | int) -> str:
expire = datetime.utcnow() + timedelta(
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
)
payload = {"sub": str(subject), "exp": expire}
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
```

```python
# api/deps.py
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from core.security import decode_token
from database import get_db
from models.user import User

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
) -> User:
try:
payload = decode_token(token)
user_id = int(payload.get("sub"))
except (JWTError, TypeError, ValueError):
raise HTTPException(401, "Invalid or expired token")

result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(401, "User not found")
return user
```

---

## 五、踩坑总结(教训清单)

### 1. 依赖管理

- **永远用 `.venv/bin/pip`**,别用系统 pip
- **装带 extras 的包必须加引号**:`pip install "sqlalchemy[asyncio]"`
- **及时固化依赖**:`.venv/bin/pip freeze > requirements.txt`

### 2. Python 环境

- **uv 装的 Python 在 `/root` 下**,其他用户(如 `www`)无法访问 → 用 `UV_PYTHON_INSTALL_DIR` 指定共享目录
- **系统 Python 版本别太老**,FastAPI 要 ≥ 3.8,实际推荐 3.10+
- **用绝对路径调用 venv 里的可执行文件**,避免 PATH 问题

### 3. 宝塔 Python 项目管理器

- **启动命令栏不要写 `nohup`、`&`、重定向**,宝塔会自己守护
- **启动命令要用你自己的 venv 绝对路径**,否则宝塔会用它另建的环境
- **改代码后必须面板重启**,gunicorn 不会热加载

### 4. Alembic

- **`config.set_main_option` 放模块级别**,别放函数里
- **autogenerate 前先 import 模型**,否则检测不到
- **表已存在时用 `alembic stamp head`**,别直接 `upgrade`

### 5. 常见版本冲突

| 包 | 问题 | 修复 |
|---|---|---|
| `passlib` + `bcrypt` | 4.1+ 不兼容 | 降级 `bcrypt==4.0.1` |
| `sqlalchemy` 异步 | 缺 greenlet | 装 `sqlalchemy[asyncio]` |
| `python-jose` | 缺 cryptography | 装 `python-jose[cryptography]` |

### 6. 排查方法论

遇到错误时,**按这个顺序排查**:

1. **看日志**:`tail -50 /path/to/app.log` 或宝塔面板项目日志
2. **看时间戳**:确认日志是最新的,避免被旧错误误导
3. **前台手动跑**:绕过进程管理器,直接看真实报错
4. **本机 curl 测**:`curl http://127.0.0.1:8000/...` 绕过 Nginx
5. **分层排查**:DB → Python → gunicorn → Nginx → 客户端

---

## 六、快速检查清单(供上线前核对)

- [ ] Python 版本 ≥ 3.10
- [ ] venv 用共享目录的 Python 创建(非 `/root` 下)
- [ ] `.venv` 属主是 `www`,可执行文件有 `x` 权限
- [ ] 所有依赖装齐(`greenlet` / `jose` / `bcrypt==4.0.1`)
- [ ] `main.py` 里没有 `create_all`
- [ ] Alembic `env.py` 里 `set_main_option` 在模块级别
- [ ] MySQL 用户对目标库有 GRANT 权限
- [ ] 宝塔启动命令用绝对路径、无 nohup
- [ ] Nginx 反代配置了 `X-Forwarded-For` / `X-Forwarded-Proto`
- [ ] SSL 证书已申请,强制 HTTPS 已开
- [ ] `requirements.txt` 已固化
- [ ] 数据库定期备份

---

## 七、结语

这次部署最大的体会是:

> **部署失败 90% 不是代码问题,而是环境问题。**

Python 版本、venv 路径、依赖 extras、权限、进程管理器、反向代理——每一个环节都可能出问题。而问题的**排查方法**比解决方案本身更重要:**看日志、看时间戳、前台跑、本机测、分层排查**。

希望这篇文章能帮你少走一些弯路。

---

*本文记录于 2026 年 10 月,环境为阿里云 ECS + 宝塔面板。*