347 lines
9.7 KiB
Markdown
347 lines
9.7 KiB
Markdown
# 开发 HTTPS 调试部署
|
||
|
||
语音预览要求麦克风可用,浏览器只在 **安全上下文**(localhost 或 https)下放行
|
||
`getUserMedia`。本机用 localhost 就够;**局域网或远程机器用 IP/域名给别的设备测,
|
||
就要走 https**。开发环境推荐 nginx 统一做 TLS 入口,前端页面、HTTP API 和 WS 信令都同源。
|
||
|
||
## 结构
|
||
|
||
```
|
||
浏览器 ──https/wss──> nginx :443 (唯一 TLS 入口, mkcert 证书)
|
||
├── /ws/ → 后端 :8000 (/ws/voice 信令、/ws/stream 裸流)
|
||
├── /api/ → 后端 :8000 (assistants/model-resources/...)
|
||
├── /health → 后端 :8000
|
||
└── / → 前端 :3000 (Next dev + HMR)
|
||
```
|
||
|
||
前端页面和信令 ws 同源(同 host 同端口),没有混合内容 / 跨源问题。
|
||
|
||
## 路径 A:本机 / 局域网,nginx 直接跑在宿主机
|
||
|
||
适合 Mac 本机调试,或同一局域网里手机访问电脑 IP。
|
||
|
||
```bash
|
||
# 1) 装 mkcert(只需一次)
|
||
brew install mkcert nss
|
||
|
||
# 2) 生成证书(本机 CA + localhost/LAN IP/ai-video.local 的证书)
|
||
./deploy/setup-certs.sh
|
||
|
||
# 3) 起前后端(任选其一)
|
||
docker compose up -d # api:8000 + ui:3030 都发布到 localhost
|
||
# 或本地分别 npm run dev / uvicorn app:app --port 8000
|
||
|
||
# 4) 起 nginx(装一下:brew install nginx)
|
||
nginx -c "$(pwd)/deploy/nginx/ai-video.dev.conf" -g 'daemon off;'
|
||
|
||
# 5) 访问
|
||
# 本机: https://localhost 或 https://ai-video.local
|
||
# 局域网:https://<本机IP> (脚本结尾会打印)
|
||
```
|
||
|
||
`deploy/nginx/ai-video.dev.conf` 里的证书路径是本机绝对路径。换目录或换机器时,
|
||
把 `ssl_certificate` 和 `ssl_certificate_key` 改到当前仓库的 `deploy/certs/`。
|
||
|
||
## 路径 B:远程开发机,Docker nginx + 宿主机 npm/uvicorn
|
||
|
||
适合在云主机/远程 Linux 上开发:数据库可以用 Docker,但前端和后端单独跑,
|
||
便于热更新和看日志。nginx 跑在 Docker 容器里,通过 `host.docker.internal`
|
||
反代到宿主机上的 `3000/8000`。
|
||
|
||
### 1. 准备证书
|
||
|
||
有域名时推荐使用正式证书,把证书文件放到:
|
||
|
||
```text
|
||
deploy/certs/ai-video.pem
|
||
deploy/certs/ai-video-key.pem
|
||
```
|
||
|
||
只做开发调试也可以用 mkcert。远程机器上把访问用的 IP/域名传给脚本:
|
||
|
||
```bash
|
||
./deploy/setup-certs.sh <远程机器公网IP或内网IP> <可选域名>
|
||
```
|
||
|
||
mkcert 是本地 CA。浏览器所在设备要免警告,需要信任这台远程机器生成的
|
||
`rootCA.pem`;否则可以临时接受浏览器的自签证书警告。
|
||
|
||
### 2. 启动数据库并初始化
|
||
|
||
```bash
|
||
docker compose up -d postgres
|
||
make db-seed
|
||
```
|
||
|
||
`make db-seed` 会建表、同步接口定义、写入模型资源和助手示例。模型密钥不放
|
||
`.env`,启动后到前端「组件 / 模型」里把 `replace-me` 换成真实密钥。
|
||
|
||
### 3. 单独启动后端
|
||
|
||
后端必须监听 `0.0.0.0`,否则 nginx 容器连不到宿主机端口:
|
||
|
||
```bash
|
||
cd backend
|
||
cp .env.example .env
|
||
|
||
# 如果 postgres 用 docker compose,宿主机连库可用 localhost:5432
|
||
# backend/.env:
|
||
# DATABASE_URL=postgresql+asyncpg://postgres:postgres@localhost:5432/postgres
|
||
|
||
uv run --with-requirements requirements.txt \
|
||
uvicorn app:app --host 0.0.0.0 --port 8000 --reload
|
||
```
|
||
|
||
### 4. 单独启动前端
|
||
|
||
前端也监听 `0.0.0.0`。`NEXT_PUBLIC_API_BASE_URL` 只有一个规则:
|
||
**写浏览器地址栏里访问 nginx 的同源地址**。如果地址栏带端口,这里也必须带端口;
|
||
如果地址栏不带端口,这里也不带端口。不要写后端直连端口 `:8000`。
|
||
|
||
Next dev 还会校验开发资源的来源。远程访问时,把浏览器地址栏里的 host 加到
|
||
`frontend/next.config.ts` 的 `allowedDevOrigins`,并重启前端 dev server:
|
||
|
||
```ts
|
||
// frontend/next.config.ts
|
||
const nextConfig = {
|
||
allowedDevOrigins: ["127.0.0.1", "<远程机器IP或域名>"],
|
||
};
|
||
```
|
||
|
||
如果漏掉这一步,页面 HTML 可能能打开,但客户端 JS 无法接管:按钮无响应,
|
||
列表一直停在 `正在加载`,同时 Next dev 内部资源可能返回 `403 Unauthorized`。
|
||
|
||
```bash
|
||
cd frontend
|
||
npm install
|
||
|
||
# nginx 使用默认 443 时:
|
||
NEXT_PUBLIC_API_BASE_URL=https://<远程机器IP或域名> \
|
||
npm run dev -- --hostname 0.0.0.0
|
||
```
|
||
|
||
### 5. 启动 Docker nginx
|
||
|
||
推荐用 compose profile:
|
||
|
||
```bash
|
||
docker compose --profile remote-dev up -d nginx-remote-dev
|
||
```
|
||
|
||
如果远程机器希望用非标准 HTTPS 端口,例如 `18189`,把宿主机端口映射到
|
||
容器的 `443` 即可:
|
||
|
||
```bash
|
||
NGINX_HTTPS_PORT=18189 docker compose --profile remote-dev up -d nginx-remote-dev
|
||
```
|
||
|
||
此时前端也要使用带端口的同源地址:
|
||
|
||
```bash
|
||
NEXT_PUBLIC_API_BASE_URL=https://<远程机器IP或域名>:18189 \
|
||
npm run dev -- --hostname 0.0.0.0
|
||
```
|
||
|
||
浏览器访问:
|
||
|
||
```text
|
||
https://<远程机器IP或域名>:18189
|
||
```
|
||
|
||
也可以直接 `docker run`:
|
||
|
||
```bash
|
||
docker run --rm --name ai-video-nginx \
|
||
--add-host=host.docker.internal:host-gateway \
|
||
-p 80:80 -p 18189:443 \
|
||
-v "$PWD/deploy/nginx/ai-video.remote-dev.conf:/etc/nginx/nginx.conf:ro" \
|
||
-v "$PWD/deploy/certs:/etc/nginx/certs:ro" \
|
||
nginx:alpine
|
||
```
|
||
|
||
访问:
|
||
|
||
```text
|
||
https://<远程机器IP或域名>:18189
|
||
```
|
||
|
||
### 6. 部署 coturn(TURN 中继)
|
||
|
||
如果浏览器和后端不在同一内网,WebRTC 媒体链路通常需要 TURN。项目里的
|
||
`docker-compose.yaml` 已经带了 coturn,用 `remote` profile 启动即可。
|
||
|
||
#### 6.1 确认公网/私网 IP
|
||
|
||
查询云主机默认路由使用的私网 IP:
|
||
|
||
```bash
|
||
ip -4 route get 1.1.1.1
|
||
```
|
||
|
||
输出中 `src` 后的地址是 `PRIVATE_IP`。例如:
|
||
|
||
```text
|
||
1.1.1.1 via 10.0.0.1 dev eth0 src 10.0.0.8
|
||
```
|
||
|
||
云控制台中绑定到该主机、用于 SSH 的公网地址是 `PUBLIC_IP`。
|
||
|
||
#### 6.2 配项目根目录 `.env`
|
||
|
||
Docker Compose 会把项目根 `.env` 中的 ICE 配置同时传给 API 和 coturn:
|
||
|
||
```text
|
||
PUBLIC_IP=<远程机器公网IP>
|
||
PRIVATE_IP=<远程机器私网IP>
|
||
TURN_SECRET=<一串足够长的随机密钥>
|
||
STUN_URL=stun:<远程机器公网IP>:3478
|
||
TURN_URLS=turn:<远程机器公网IP>:3478?transport=udp,turn:<远程机器公网IP>:3478?transport=tcp
|
||
```
|
||
|
||
示例:
|
||
|
||
```text
|
||
PUBLIC_IP=118.89.89.89
|
||
PRIVATE_IP=10.0.0.8
|
||
TURN_SECRET=change-me-to-a-long-random-string
|
||
STUN_URL=stun:118.89.89.89:3478
|
||
TURN_URLS=turn:118.89.89.89:3478?transport=udp,turn:118.89.89.89:3478?transport=tcp
|
||
```
|
||
|
||
`STUN_URL` 指向同机 coturn,可避免后端等待不可达的公共 STUN。
|
||
Compose 的 `environment` 会覆盖 `backend/.env` 中的同名值,因此 Docker
|
||
部署必须在项目根 `.env` 设置上述变量。
|
||
|
||
#### 6.3 启动 coturn
|
||
|
||
```bash
|
||
docker compose config --quiet
|
||
docker compose --profile remote up -d --build --force-recreate api coturn
|
||
```
|
||
|
||
查看状态和日志:
|
||
|
||
```bash
|
||
docker compose ps coturn
|
||
docker compose logs -f coturn
|
||
```
|
||
|
||
#### 6.4 放行端口
|
||
|
||
云安全组至少放行:
|
||
|
||
```text
|
||
80/tcp
|
||
443/tcp
|
||
3478/tcp
|
||
3478/udp
|
||
49152-49200/udp
|
||
```
|
||
|
||
如果 HTTPS 映射到 `18189`,还要放行:
|
||
|
||
```text
|
||
18189/tcp
|
||
```
|
||
|
||
如果远程机器启用了 `ufw`:
|
||
|
||
```bash
|
||
sudo ufw allow 80/tcp
|
||
sudo ufw allow 443/tcp
|
||
sudo ufw allow 18189/tcp
|
||
sudo ufw allow 3478/tcp
|
||
sudo ufw allow 3478/udp
|
||
sudo ufw allow 49152:49200/udp
|
||
```
|
||
|
||
#### 6.5 验证后端已下发 TURN
|
||
|
||
后端和 nginx 都跑起来后:
|
||
|
||
```bash
|
||
curl -k https://<远程机器IP或域名>:18189/api/webrtc/ice-servers
|
||
```
|
||
|
||
应该能看到 `stun:` 和 `turn:`:
|
||
|
||
```json
|
||
{
|
||
"iceServers": [
|
||
{"urls": "stun:118.89.89.89:3478"},
|
||
{
|
||
"urls": "turn:<远程机器公网IP>:3478?transport=udp",
|
||
"username": "...",
|
||
"credential": "..."
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
## 前端怎么连后端
|
||
|
||
前端读环境变量 `NEXT_PUBLIC_API_BASE_URL`。走 nginx 后,让它等于浏览器访问
|
||
页面的 **origin** 即可。origin 包含协议、host 和可选端口:
|
||
|
||
```
|
||
默认 443: NEXT_PUBLIC_API_BASE_URL=https://<访问用的host>
|
||
自定义端口: NEXT_PUBLIC_API_BASE_URL=https://<访问用的host>:18189
|
||
|
||
wsUrl = NEXT_PUBLIC_API_BASE_URL.replace(/^http/, 'ws') + '/ws/voice'
|
||
```
|
||
|
||
> 没有反代、直连后端时则是 `https://<host>:8000`,但那样要给后端单独配证书、
|
||
> 还有跨源,不推荐。统一走 nginx 最干净。
|
||
|
||
## 给别的设备免警告
|
||
|
||
证书是 mkcert 本地 CA 签的,只有装了该 CA 的设备才信任:
|
||
|
||
```bash
|
||
mkcert -CAROOT # 打印 CA 目录,里面的 rootCA.pem 拷到设备并信任
|
||
```
|
||
|
||
LAN IP 不在证书 SAN 里会报名称不匹配——`setup-certs.sh` 已自动把探测到的
|
||
en0/en1 IP 加进 SAN;换网络换了 IP,重跑脚本即可。
|
||
|
||
## 证书不入库
|
||
|
||
`deploy/.gitignore` 已忽略 `certs/`。私钥不要提交。
|
||
|
||
## 常见问题
|
||
|
||
### nginx 容器里为什么不用 127.0.0.1?
|
||
|
||
Docker 容器里的 `127.0.0.1` 是容器自己,不是宿主机。远程开发路径使用
|
||
`host.docker.internal`,并通过 `host-gateway` 映射到宿主机。
|
||
|
||
### 页面能打开,但 HMR 或语音 WS 断开?
|
||
|
||
检查 nginx 配置里是否保留了 `Upgrade`、`Connection` 头,以及 `/ws/` 的
|
||
`proxy_read_timeout 3600s`。本仓库的两份开发 nginx 配置都已经包含。
|
||
|
||
### 页面一直正在加载,但 API 直接访问正常?
|
||
|
||
先确认前端 dev server 已用浏览器访问同源地址启动:
|
||
|
||
```bash
|
||
NEXT_PUBLIC_API_BASE_URL=https://<远程机器IP或域名>:18189 \
|
||
npm run dev -- --hostname 0.0.0.0
|
||
```
|
||
|
||
再确认 `frontend/next.config.ts` 的 `allowedDevOrigins` 包含同一个 host:
|
||
|
||
```ts
|
||
allowedDevOrigins: ["127.0.0.1", "<远程机器IP或域名>"],
|
||
```
|
||
|
||
修改 `NEXT_PUBLIC_API_BASE_URL` 或 `allowedDevOrigins` 后都必须重启
|
||
`npm run dev`。若 API 直接返回 `200`,但页面按钮无响应、弹窗打不开、列表
|
||
仍停在 `正在加载`,通常是 Next dev 的 `_next/*` 客户端资源被跨源保护拦截,
|
||
不是后端接口没有数据。
|
||
|
||
### API 请求跨域报错?
|
||
|
||
走 nginx 时前端和后端同源,通常不需要跨源。若你绕过 nginx 直接访问后端,
|
||
需要在 `backend/.env` 的 `CORS_ORIGINS` 加上前端实际 origin。
|