From f00c941eded27cb4b5e1a08c58674d6a90e4c86c Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 26 Aug 2026 10:25:15 -0600 Subject: [PATCH] =?UTF-8?q?perf(=E6=9E=84=E5=BB=BA):=20builder=20=E5=88=86?= =?UTF-8?q?=E5=BC=80=E6=8B=B7=E6=BA=90=E7=A0=81=EF=BC=8C=E5=8F=AA=E6=94=B9?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=B8=8D=E5=86=8D=E9=99=AA=E7=BC=96=E4=B8=80?= =?UTF-8?q?=E6=AC=A1=E5=89=8D=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit builder 阶段是 `COPY . .` 一把梭,而 docker 的层缓存是链式的:那一层的校验和 覆盖整个仓库,改任何一个文件都会把它作废,后面 vite build 和 bun compile 全部 重跑。前端那 160s 一次都躲不掉,改一篇 markdown 也要陪着编。 按各自的输入分开拷,慢的那条放前面(缓存链上越靠前越不容易被碰到): 只改 apps/api 前端两层命中缓存,只重编后端 ~1s 只改 apps/web 前端重编,后端被链条带着 ~160s 改 packages/contract 两边都重编(本来就都依赖它,是对的) 改 docs / 根上的杂项 builder 根本看不见 0 代价是 builder 只看得见这里点名拷进来的四样,新加顶层目录要同步加 COPY。 顺带堵掉两个同源的洞: · apps/web/docs、tests、**/CLAUDE.md 进了 .dockerignore。它们在 apps/web 里面, 整个目录拷进去的话,改一篇文档也会让 vite build 那层失效。 · .dockerignore 末尾为 --prebuilt 开的例外(!dist/oj2-api、!apps/web/dist)让本地 产物照样进构建上下文 —— 这是比 `COPY . .` 更隐蔽的一条:在本机编一次后端, 80MB 的 dist/oj2-api 变了,rsync 上去,服务器就重编一次前端,源码一行没动。 dist/oj2-api 现在不再被 builder 拷贝;apps/web/dist 仍在 COPY apps/web 里面, 所以 deploy.sh 头上那条手动 rsync 补了 --exclude dist。CI 走 --prebuilt, builder 整个不进构建图,不受影响。 验证:用老 Dockerfile 和新 Dockerfile 各构建一次 --target artifacts 导出对比, 后端二进制 sha256 相同,前端 dist 500 个文件逐个 sha256 一致。 Co-Authored-By: Claude Opus 5 --- .dockerignore | 13 +++++++++++++ docker/Dockerfile | 23 ++++++++++++++++++----- docker/deploy.sh | 9 ++++++++- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.dockerignore b/.dockerignore index 5b3eba3..08d6247 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,6 +28,14 @@ docker/.env.* # 前端 dist 由构建阶段自己生成,本地那份不要带进去污染 apps/web/dist/ +# apps/ 下的非构建输入。Dockerfile 里是整个目录拷进去的(`COPY apps/web apps/web`), +# 所以改一篇 docs/ 里的 markdown 也会让 `RUN vite build` 那层失效,白编 160s。 +# 这几样都不被源码 import,挡在上下文外面最省事。 +apps/web/docs/ +apps/web/tests/ +**/CLAUDE.md +**/README.md + # —— 例外:ARTIFACTS=prebuilt 时,产物本身就是构建输入 —— # # 这几条必须放在文件最后:dockerignore 是**最后一条匹配的规则说了算**, @@ -35,6 +43,11 @@ apps/web/dist/ # # ARTIFACTS=build(默认,服务器手动部署走的那条)时它们只是让上下文多传 # 几十 MB,镜像里用不到;本地根本没编过的话这两个路径不存在,也不影响。 +# +# ⚠️ 一个代价:`apps/web/dist` 因此会进 builder 的 `COPY apps/web` 那一层, +# 也就进了它的缓存键 —— 手边有一份内容不同的本地 dist,就会白重编一次前端。 +# 所以 docker/deploy.sh 头上那条手动 rsync 用 --exclude dist 把它挡在服务器外。 +# (CI 走 --prebuilt,builder 整个不进构建图,不受这条影响。) !dist/oj2-api !apps/web/dist !apps/web/dist/** diff --git a/docker/Dockerfile b/docker/Dockerfile index aece53f..4991456 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -62,17 +62,30 @@ RUN bun install --frozen-lockfile \ || bun install --frozen-lockfile \ || bun install --frozen-lockfile -COPY . . +# 源码**分开拷**,别用 `COPY . .`:那样任何一个文件变了都会一次作废下面两条 +# 构建,只改了后端也得陪着重编一次前端(160s)。 +# +# 顺序是「先前端后后端」,因为 Dockerfile 的缓存是链式的 —— 一层失效,后面全失效。 +# 慢的那条要待在更靠前、更不容易被碰到的位置: +# 只改 apps/api → 前端两层命中缓存,只重编后端(bun compile,几秒) +# 只改 apps/web → 前端重编,后端跟着重编,但那点时间无所谓 +# 改 packages/contract → 两边都重编(本来就都依赖它,是对的) +# +# 加新的顶层目录 / 根文件时注意:builder 只看得见这里点名拷进来的东西。 +COPY tsconfig.base.json ./ +COPY packages/contract packages/contract + +# 前端:产物直接进镜像,不走挂载。 +# 一次构建 = 一个版本,切换那天不会出现「后端换了前端忘了拷」这种半新半旧状态。 +COPY apps/web apps/web +RUN cd apps/web && bun run build # 后端:编译成不依赖 node_modules 的单二进制。 # 能这么编是因为 wasm / 原生模块 / 词典都在源码里用 `with { type: "file" }` # 内嵌成了资源,详见 apps/api/src/vendor/jieba.ts 的注释。 +COPY apps/api apps/api RUN bun build --compile --target=bun-linux-x64 apps/api/src/main.ts --outfile /build/oj2-api -# 前端:产物直接进镜像,不走挂载。 -# 一次构建 = 一个版本,切换那天不会出现「后端换了前端忘了拷」这种半新半旧状态。 -RUN cd apps/web && bun run build - # ---------------------------------------------------------------- 产物来源 # 两个来源都归一成同样的布局(/artifacts/oj2-api、/artifacts/web),下面的运行时 # 阶段只认这个布局,不关心产物是谁编的。 diff --git a/docker/deploy.sh b/docker/deploy.sh index 49fdd14..2feb89c 100755 --- a/docker/deploy.sh +++ b/docker/deploy.sh @@ -18,7 +18,14 @@ # 代码怎么上到服务器不归它管。没有 git remote 的话,在本机: # # rsync -az --delete --exclude node_modules --exclude .git --exclude data \ -# --exclude 'docker/.env*' ~/Projects/OJ/OJ2/ root@服务器:/root/OJDeploy/OJ2/ +# --exclude 'docker/.env*' --exclude dist --exclude 'apps/web/dist' \ +# ~/Projects/OJ/OJ2/ root@服务器:/root/OJDeploy/OJ2/ +# +# 那两条 --exclude dist 别去掉。不带 --prebuilt 时服务器上根本不需要本机的产物, +# 而 .dockerignore 末尾为 prebuilt 开的例外(`!apps/web/dist`)让它照样进构建上下文 +# ——于是本机重编一次前端,服务器上 `COPY apps/web` 那层的缓存键就变了,明明没改 +# 源码也要重编 160s。CI 那条路(--prebuilt)反过来必须传产物,它自己的 rsync 在 +# .github/workflows/deploy.yml 里,别照抄这条。 # # 前提:docker/.env 已经填好(内容见 docs/specs/phase5-cutover-runbook.md 第三节)。