Skip to content

feat(deploy): 让 main 上的后端可直接部署并被浏览器访问 - #115

Open
johnnyzhang-eng wants to merge 3 commits into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/deployable-backend
Open

feat(deploy): 让 main 上的后端可直接部署并被浏览器访问#115
johnnyzhang-eng wants to merge 3 commits into
1024XEngineer:mainfrom
johnnyzhang-eng:feat/deployable-backend

Conversation

@johnnyzhang-eng

Copy link
Copy Markdown

变更内容

三个文件,让 main 上的后端可以一条命令起起来并被前端连上:

  • 新增 backend/Dockerfile
  • 新增 docker-compose.yml
  • bootstrap/app.py 挂 CORS 中间件

不依赖任何未合入的分支,直接基于当前 main

为什么单独提这个 PR

main 上目前没有 Dockerfile 也没有 docker-compose.yml,后端无法部署;而 CORS 缺失会让浏览器把前端所有请求拦在预检那一步。这三样是「服务能起来、前端能连上」的最小集合,与后端业务功能正交,因此单独提,不必等更大的后端 PR。

三条不是照搬模板的实现细节

都是在一台真实云主机上部署时踩出来的。

1. builder 与 runtime 必须同路径。

uv 装出来的 venv 里,可执行脚本的 shebang 与 workspace 包的 .pth 都是绝对路径。builder 在 /build、runtime 在 /app 时,容器起来会反复重启并报:

exec /app/.venv/bin/uvicorn: no such file or directory

这条最不直观 —— 那个文件其实存在,报的是它 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 . All checks passed
uv run pytest -q 1 passed
TestClient 验 CORS OPTIONS 预检 200,access-control-allow-origin 正确

在真实服务器上,用本 PR 的代码树完整验过一遍(验证用的容器与镜像已清除):

docker build ./backend            → 成功
docker run -p 8099:8000           → Up
GET /docs                         → HTTP 200
OPTIONS /media/upload (跨域预检)   → HTTP 200,allow-origin 正确

待对齐

  • CORS 生产来源:默认值只覆盖本地 dev(5173 / 3000)并放行 Vercel 预览域名。正式域名定了之后用 WINDUP_CORS_ORIGINS 追加,不需要改代码。
  • 本 PR 只保证「服务起得来、浏览器连得上」。业务端点的实现在其它 PR。

让 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>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 5, 2026 2:15am

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the deployment and CORS changes. I found two concrete issues, both left inline.

Verification: inspected the fixed PR diff 023bf0f86e8089681ae9a09427a12306b3c0b3dc...8c15ea634ba70c9dd2faf8327e891118518de0f9; git diff --check passed.

View job run

Comment thread backend/Dockerfile
RUN uv sync --frozen --no-dev --no-install-workspace

COPY packages/ packages/
RUN uv sync --frozen --no-dev

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@johnnyzhang-eng johnnyzhang-eng Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认,已在 6333adb 修。

server/media/service.py 函数体里延迟 import qiniu,pyproject 和 uv.lock 都没有它 —— 镜像能构建、能启动、/docs 也返回 200,只有第一次 POST /media/uploadModuleNotFoundError,正是你描述的路径。

  • 声明位置放在 app 包(import 发生在这里):packages/app/pyproject.tomlqiniu>=7.13
  • uv.lock 重新锁定,只新增 qiniu 7.18.0 一项(它唯一的依赖 requests 已在锁内)
  • CI 加断言 importlib.util.find_spec("qiniu") is not Nonetests/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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

确认,已在 6333adb 修。

复核了 starlette 1.3.1 的 CORSMiddlewareallow_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。

Johnny Zhang and others added 2 commits August 5, 2026 09:53
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant