feat(deploy): 让 main 上的后端可直接部署并被浏览器访问 - #115
Conversation
让 main 上的后端可以直接部署起来并被浏览器访问。三处内容都是在一台真实云主机上
部署时踩出来的,不是照搬模板:
1. backend/Dockerfile —— builder 与 runtime **必须同路径**。uv 装出来的 venv 里,
可执行脚本 shebang 与 workspace 包 .pth 都是绝对路径;builder 在 /build、
runtime 在 /app 时,容器起来会报 "exec /app/.venv/bin/uvicorn: no such file or
directory" —— 报的不是脚本本身而是它 shebang 指向的解释器,同时 workspace 包
import 不到。两边都用 /app 即可,不需要任何 relocate 技巧。
另加国内镜像源与 UV_HTTP_TIMEOUT=180:实测宿主机访问 pypi.org 需 8s,
构建容器内默认超时会在下载 uvloop 时直接失败。
2. docker-compose.yml —— 网络显式设 mtu 1450。云主机链路 MTU 常小于 1500
(实测某部署机 eno1 为 1480),而 compose 自建网络**不继承** daemon.json 的
mtu 设置,默认仍是 1500,大包被丢,表现为 TLS 握手超时(对象存储上传挂死、
pip 下载卡死)而不是明确报错。这条是分两次才定位到的:先改 daemon 以为好了,
重建后容器里仍是 1500。
POSTGRES_PASSWORD 用 ${VAR:?} 强制显式提供,不给默认值。
3. CORS 中间件 —— 不挂的话浏览器会把前端**所有**请求拦在预检那一步
(OPTIONS 返回 405、响应无 access-control-* 头),且后端日志里连请求都看不到,
很容易被误判成前端问题。来源用 WINDUP_CORS_ORIGINS 覆盖,默认覆盖本地 dev
并放行 Vercel 预览域名。
本地验证:ruff 通过;pytest 1 passed;TestClient 实测 OPTIONS 预检返回 200 且
allow-origin 正确。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
| RUN uv sync --frozen --no-dev --no-install-workspace | ||
|
|
||
| COPY packages/ packages/ | ||
| RUN uv sync --frozen --no-dev |
There was a problem hiding this comment.
The image installs only the locked production dependencies here, but the upload path imports qiniu lazily in windup_app.server.media.service, and neither backend/uv.lock nor any backend pyproject.toml declares that package. The container can start, but /media/upload will fail at request time with ModuleNotFoundError: qiniu. Please add the SDK to the appropriate package and update the lockfile so the deployable image contains the runtime dependency.
There was a problem hiding this comment.
确认,已在 6333adb 修。
server/media/service.py 函数体里延迟 import qiniu,pyproject 和 uv.lock 都没有它 —— 镜像能构建、能启动、/docs 也返回 200,只有第一次 POST /media/upload 才 ModuleNotFoundError,正是你描述的路径。
- 声明位置放在 app 包(import 发生在这里):
packages/app/pyproject.toml加qiniu>=7.13 uv.lock重新锁定,只新增qiniu 7.18.0一项(它唯一的依赖requests已在锁内)- CI 加断言
importlib.util.find_spec("qiniu") is not None(tests/test_deployable_backend.py),避免同类「延迟 import 未声明」再溜过去
本机没有 docker,未重建镜像验证;改用 uv export --frozen --no-dev 确认 qiniu==7.18.0 在生产解析集内 —— Dockerfile 里 uv sync --frozen --no-dev 装的正是这一集。
另:对象存储配置(StorageSettings)在 framework 层,SDK 却因 import 位置声明在 app 层,这处分层不一致本 PR 没动(属于 media 服务归属问题,与部署正交),需要一并归位到 framework 的话我再提。
| app.add_middleware( | ||
| CORSMiddleware, | ||
| allow_origins=_cors_origins(), | ||
| allow_origin_regex=r"https://.*\.vercel\.app", |
There was a problem hiding this comment.
This regex still allows every https://*.vercel.app origin even when WINDUP_CORS_ORIGINS is set to a specific production frontend, and allow_credentials=True is enabled below. Any unrelated Vercel app can therefore make credentialed browser requests to this backend if a user has backend credentials. Restrict this to known project/preview domains, or make the regex configurable together with the explicit origin list.
There was a problem hiding this comment.
确认,已在 6333adb 修。
复核了 starlette 1.3.1 的 CORSMiddleware:allow_origin_regex 走的是 fullmatch,所以 https://.*\.vercel\.app 精确地就是「整个 vercel.app 域下任意应用」,且与 allow_origins 是或关系 —— 显式配了 WINDUP_CORS_ORIGINS 也关不掉它,正如你指出的;下面又开着 allow_credentials=True,等于把带凭证的跨域请求权限交出去。
改法(两条都做了):
- 正则不再写死在代码里,改由
WINDUP_CORS_ORIGIN_REGEX提供,默认为空即不启用;不配的话只有WINDUP_CORS_ORIGINS(或本地 dev 默认值)里的显式来源能过。 docker-compose.yml透传该变量并注明:只写自家项目的预览域名形态(如https://<项目名>-[a-z0-9-]+\.vercel\.app),不要写成整个平台通配。没把具体域名硬编码进代码,是因为预览域名形态随部署环境变,写进代码等于把某一个托管账号焊死在后端里。
tests/test_deployable_backend.py 钉了三条预检断言:配置内来源放行;陌生来源默认拿不到 access-control-allow-origin;预览正则显式开启后只匹配自家形态、陌生来源仍被拒。本地 pytest 5 passed。
…origins
按评审意见修两处「容器起得来但请求会挂/放得太开」的问题。
1. qiniu 没声明依赖。server/media/service.py 在函数体里延迟 import qiniu,
pyproject 与 uv.lock 都没有它 —— 镜像能构建、能启动、/docs 也正常,
直到第一次 POST /media/upload 才 ModuleNotFoundError。声明在 app 包
(import 发生在这里);uv.lock 只新增 qiniu 7.18.0 一项,requests 已在锁内。
2. CORS 不再写死 https://.*\.vercel\.app。starlette 用 fullmatch,那条正则
放行整个 vercel.app 域下的任意第三方应用,而这里 allow_credentials=True,
且显式配了 WINDUP_CORS_ORIGINS 也关不掉它。改为 WINDUP_CORS_ORIGIN_REGEX
提供、默认不开;compose 里透传该变量并注明只写自家项目的域名形态。
新增 tests/test_deployable_backend.py 把两条钉在 CI:find_spec("qiniu")
断言运行期依赖装齐;三条 CORS 预检断言(配置内来源放行 / 陌生来源默认拿不到
allow-origin / 预览正则需显式开启且只匹配自家形态)。
验证:ruff 通过;lint-imports 2 kept;pytest 5 passed。本机无 docker,
未重建镜像,改以 `uv export --frozen --no-dev` 确认 qiniu 在生产解析集内
(Dockerfile 装的正是这一集)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
演示跑的是生产构建 `vite preview`,端口 4173,而默认放行列表只有 dev 的 5173 和 3000。实测已部署的后端对 `Origin: http://localhost:4173` 的预检直接 400、 不带 access-control-allow-origin —— 前端每个请求都会被浏览器拦在预检那一步, 后端日志里连请求都看不到,最容易被误判成后端挂了。 默认值补上 4173(localhost 与 127.0.0.1 各一条),并加一条断言钉住。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
变更内容
三个文件,让
main上的后端可以一条命令起起来并被前端连上:backend/Dockerfiledocker-compose.ymlbootstrap/app.py挂 CORS 中间件不依赖任何未合入的分支,直接基于当前
main。为什么单独提这个 PR
main上目前没有Dockerfile也没有docker-compose.yml,后端无法部署;而 CORS 缺失会让浏览器把前端所有请求拦在预检那一步。这三样是「服务能起来、前端能连上」的最小集合,与后端业务功能正交,因此单独提,不必等更大的后端 PR。三条不是照搬模板的实现细节
都是在一台真实云主机上部署时踩出来的。
1. builder 与 runtime 必须同路径。
uv 装出来的 venv 里,可执行脚本的 shebang 与 workspace 包的
.pth都是绝对路径。builder 在/build、runtime 在/app时,容器起来会反复重启并报:这条最不直观 —— 那个文件其实存在,报的是它 shebang 里指向的
/build/.venv/bin/python;同时 workspace 包也 import 不到。两边都用/app即可,不需要任何 relocate 技巧。2. compose 网络要显式设 MTU。
云主机链路 MTU 常小于 1500(实测某部署机
eno1为 1480)。而 compose 自建网络不继承daemon.json里的mtu设置,默认仍是 1500 → 大包被丢,表现为 TLS 握手超时(对象存储上传挂死、pip 下载卡死)而不是明确报错。这条是分两次才定位到的:先改了 daemon 以为好了,重建后容器里仍是 1500。所以必须写在 compose 的
networks.driver_opts里。3. 国内网络要换源并拉长超时。
实测宿主机访问
pypi.org需 8s,构建容器内默认超时会在下载大包(uvloop)时operation timed out直接失败。另外
POSTGRES_PASSWORD用${VAR:?}强制显式提供,不给默认值 —— 避免默认弱口令跟着编排一起进生产。验证
本地(基于本 PR 的分支):
uv run ruff check .uv run pytest -qaccess-control-allow-origin正确在真实服务器上,用本 PR 的代码树完整验过一遍(验证用的容器与镜像已清除):
待对齐
WINDUP_CORS_ORIGINS追加,不需要改代码。