Skip to content

build(deploy): containerize the frontend and add it to docker compose - #187

Merged
huyanxius merged 6 commits into
1024XEngineer:mainfrom
huyanxius:feat/185-frontend-docker-deploy
Aug 11, 2026
Merged

build(deploy): containerize the frontend and add it to docker compose#187
huyanxius merged 6 commits into
1024XEngineer:mainfrom
huyanxius:feat/185-frontend-docker-deploy

Conversation

@huyanxius

Copy link
Copy Markdown
Collaborator

前端补上容器化部署:新增前端镜像,并作为第四个服务接入现有的 docker-compose.yml。服务器上 docker compose up -d --build 一条命令即可连前端一起起来,不再依赖 Vercel。

Why

#162 把 Redis、PostgreSQL 和后端编排进了 compose,前端不在其中。前端此前只有 Vercel 一条部署路径(frontend/vercel.json),服务器上没有它的产物,整套系统在一台机器上起不完整。

次要动机是跨域。前端与后端不同源时,浏览器对非简单请求先发预检,后端需维护来源白名单,#139 与尚未关闭的 #184 都出在这条链路上。容器内反代让两者同源,这条链路不再被触发。

Changes

  • 新增前端镜像:Node 阶段构建,nginx 阶段只带 dist/,产物 22 MB。
  • compose 新增 frontend 服务,经 WINDUP_WEB_PORT(默认 8080)访问,子路由直接访问不再 404。
  • 前端接口请求走 /api/,由容器内 nginx 转发至 backend:8000,后端无需为前端配置 CORS 来源。
  • .env.example 增加 WINDUP_WEB_PORTVITE_API_BASE_URLfrontend/README.md 补容器部署说明。

Implementation

后端地址在构建期确定。VITE_ 前缀的变量由 vite build 写入产物,运行期注入环境变量对已构建的 JS 无效,因此它放在 compose 的 build.args 而非 environment。默认值 /api 是相对路径,镜像不绑定域名,同一产物可跨环境复用;要指向外部后端则构建期传 --build-arg VITE_API_BASE_URL=…,并同步配置后端的 WINDUP_CORS_ORIGINS

反代按请求解析主机名。直写 proxy_pass http://backend:8000/ 时 nginx 只在启动时解析一次,backend 重建换 IP 后前端会持续 502,且启动时解析失败会导致 nginx 起不来。改用 resolver 127.0.0.11 加变量形式规避这两点;变量形式下 nginx 不再自动剥前缀,故配合 rewrite ^/api/(.*)$ /$1 break,query string 由 rewrite 保留。

前缀剥离对齐后端现有路由。/projects/characters/users 均挂在根路径,/api/projects 需落为 backend:8000/projects。后端若引入路由前缀,此处要同步调整。

depends_on 只等 backend 启动、不等 healthy,保证后端异常时前端静态页仍可访问。

缓存分两档:/assets/ 的文件名带内容 hash,给长缓存并标 immutable;index.html 显式 no-cache,避免发版后旧 html 引用已删除的资源导致白屏。

Verification

  • format:npm run format:check,全部文件格式正确
  • lint:npm run lint,oxlint 无告警
  • typecheck:npm run typecheck,通过
  • test:npm run test,27 个文件 191 条用例全过
  • build:docker build ./frontend 成功,镜像内执行的是 tsc -b && vite build
  • 接口验证:桩服务,非真实后端

容器级验证以一个返回 $request_uri 的 nginx 桩容器充当 backend,与前端容器同处一个 user-defined 网络:

检查 结果
backend 缺席时启动前端 容器 running,首页 200,/api/projects 502
/api/projects 桩后端收到 /projects,前缀已剥
/api/projects?page=2 桩后端收到 /projects?page=2,query 保留
/projects 直接访问 200,返回 index.html
缓存头 index.html 为 no-cache,assets/*.js 为 max-age=31536000, immutable
产物内烘进的 base function _r(e=`/api`)
backend 重建换 IP(172.18.0.3 → 172.18.0.4) /api/projects 仍然通
容器健康检查 33 秒转 healthy

健康探针最初写作 wget http://localhost/,容器始终 unhealthy:busybox 的 wget 优先解析 ::1,而 nginx 只 listen 80。已改为 127.0.0.1 并复验。

整栈四容器未在本机跑通。后端镜像构建成功,但本机存在一个 2026-07-31 由手工 docker run 创建的同名容器 windup-postgres(非 compose 创建,挂载命名卷 windup-postgres-data),占用容器名导致创建冲突,未予清理。改以 docker compose up -d --no-deps frontend 验证服务定义:容器创建正常,端口映射 0.0.0.0:8080->80,首页与子路由均 200。四容器同时 healthy 待在服务器上确认。

Screenshots

两张均取自容器(宿主机 8080 端口)。本 PR 不改界面代码,因此只提供 After。

Home Deep link

右图为直接访问 /projects:nginx 回退到 index.html,前端路由接管后按 #178 的登录守卫跳转至 /?account=login&returnTo=%2Fprojects。没有回退则此处为 404。

Scope

  • 本 PR 不包含:TLS、证书与域名配置(宿主机已有反代时由其转发至 WINDUP_WEB_PORT);vercel.json 不动,Vercel 路径照旧可用;frontend-ci.yml 不动;业务代码与 shared/api 实现不动;fix(backend): 放行受保护接口的 CORS 预检请求 #184 的后端预检问题不在本 PR 修。
  • 后续事项:在服务器上实跑整栈,确认四个容器同时 healthy。

Related Issues

Closes #185
Refs #162
Refs #184

The frontend ships no container image, so the deployment described in docker-compose.yml cannot serve it next to the backend.

Add a multi-stage Dockerfile that builds with Node and ships only dist on nginx, together with an nginx config providing SPA fallback and an /api reverse proxy to the backend service.

The image serves pages and API calls from one origin, so the deployment needs no CORS configuration at all.
Compose orchestrates Redis, PostgreSQL and the backend, leaving the frontend outside the single-server deployment.

Add a frontend service building from ./frontend on the shared network, publishing WINDUP_WEB_PORT, and passing VITE_API_BASE_URL as a build argument since Vite bakes it at build time.

One compose command now brings up the whole stack, and the example env file documents both new variables.
The README covers local development only and says nothing about how the app reaches the backend once containerized.

Describe the compose workflow, the nginx reverse proxy, and why VITE_API_BASE_URL cannot be injected at runtime.

Readers can deploy without rediscovering the build-time constraint, and the Vercel path is stated as unaffected.
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
windup Ready Ready Preview Aug 11, 2026 2:58am

@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.

Found one deployment-blocking issue in the new nginx proxy configuration.

Comment thread frontend/nginx.conf Outdated
The nginx proxy inherits a 1 MB default request body limit, so images sent to /media/upload are rejected with 413 before FastAPI receives them.

Set client_max_body_size to 20 MB on the /api/ location, leaving the static routes untouched.

Uploads up to 20 MB now reach the backend, while a 25 MB request is still refused at the proxy.
@xiaocheny214

Copy link
Copy Markdown
Contributor

方案调整建议

宿主机已在跑 nginx 负责静态资源服务和 /api 反代,前端容器再起一个 nginx 会双重反代。

建议:容器只做构建,产物通过 volume 挂到宿主机,由宿主机 nginx 直接服务。

  1. 去掉 frontend/nginx.conf,Dockerfile 最终阶段保持 node 镜像,构建完即退出
  2. docker-compose 去掉 ports 映射,改用 volume mount 把构建产物挂出
  3. npm run build -- --outDir <挂载目录> 直接输出到宿主机,不需要中间拷贝
  4. restart: "no" — 构建完退出,不需要常驻

更新时 docker compose up -d --build frontend 即可,宿主机 nginx 读的是磁盘文件,下次访问自动拿到新产物。

@huyanxius

Copy link
Copy Markdown
Collaborator Author

方案调整建议

宿主机已在跑 nginx 负责静态资源服务和 /api 反代,前端容器再起一个 nginx 会双重反代。

建议:容器只做构建,产物通过 volume 挂到宿主机,由宿主机 nginx 直接服务。

  1. 去掉 frontend/nginx.conf,Dockerfile 最终阶段保持 node 镜像,构建完即退出
  2. docker-compose 去掉 ports 映射,改用 volume mount 把构建产物挂出
  3. npm run build -- --outDir <挂载目录> 直接输出到宿主机,不需要中间拷贝
  4. restart: "no" — 构建完退出,不需要常驻

更新时 docker compose up -d --build frontend 即可,宿主机 nginx 读的是磁盘文件,下次访问自动拿到新产物。

收到,正在改

The server already owns static serving and API proxying.

Make the Node container write Vite output into the mounted web root and exit.

Deployments no longer run or expose a second nginx instance.
The previous guide still described the removed container nginx and web port.

Document the host web root, one-shot build command, and API proxy ownership.

Operators can now update frontend assets without expecting a long-running container.
@huyanxius

Copy link
Copy Markdown
Collaborator Author

方案调整建议

宿主机已在跑 nginx 负责静态资源服务和 /api 反代,前端容器再起一个 nginx 会双重反代。

建议:容器只做构建,产物通过 volume 挂到宿主机,由宿主机 nginx 直接服务。

  1. 去掉 frontend/nginx.conf,Dockerfile 最终阶段保持 node 镜像,构建完即退出
  2. docker-compose 去掉 ports 映射,改用 volume mount 把构建产物挂出
  3. npm run build -- --outDir <挂载目录> 直接输出到宿主机,不需要中间拷贝
  4. restart: "no" — 构建完退出,不需要常驻

更新时 docker compose up -d --build frontend 即可,宿主机 nginx 读的是磁盘文件,下次访问自动拿到新产物。

@xiaocheny214 已按建议调整,前端容器现在只负责构建,产物由宿主机 nginx 直接服务,麻烦再帮忙看一下

@xiaocheny214 xiaocheny214 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.

没问题,可以合并。

@huyanxius
huyanxius merged commit 0356b06 into 1024XEngineer:main Aug 11, 2026
6 checks passed
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.

feat: 前端容器化并接入 docker-compose 部署

2 participants