跳到正文
Joeplover
工程·2026-07-03·约 4 分钟阅读

Nginx 子路径部署 + 生产环境 git 冲突处理

把 8gu_killer 挂在 www.ploverjoe.xyz/8gu 下,Nginx 子路径配置的三处同步修改,以及服务器上 git pull 时保留本地数据库解决二进制文件冲突。

Server room with blue network cables and server racks

背景

今天做了一件一直在计划的事:把 8gu_killer 答题平台挂到主域名 www.ploverjoe.xyz 的子路径下,和博客共存。

域名根路径 / 跑博客(Next.js, 端口 3000),新加 /8gu 子路径跑答题平台(Vite + React 前端, Express 后端 3002)。

Nginx 子路径配置

问题:子路径的资源全都 404

一开始只加了一个 location /8gu,结果 HTML 渲染出来了,但 /@vite/client、@react-refresh、/src/main.jsx 全部 404。原因是 Vite 生成的资源路径是根绝对路径(/...),浏览器请求 /assets/index.js 时命中 location / → 被转发到博客的 3000,而博客没有这些路由。

三处同步修改

子路径部署有三个点必须同步改,少一个就崩:

① Vite base 配置 — 所有构建资源的路径前缀

// vite.config.js
export default defineConfig({
  base: '/8gu/',  // 让产物 HTML 中的资源路径变为 /8gu/assets/...
  // ...
})

② React Router basename — 前端路由的根路径

// src/main.jsx
<BrowserRouter basename="/8gu">
  <App />
</BrowserRouter>

③ Nginx location 块 — 反向代理分类

# 规范 301:/8gu → /8gu/
location = /8gu {
    return 301 /8gu/;
}

# 前端静态资源代理到 Vite 开发服务器
location /8gu/ {
    proxy_pass http://localhost:3001;
    # ... WebSocket 支持、请求头
}

# API 独立 location,避免和博客的 /api 冲突
location /8gu/api/ {
    proxy_pass http://localhost:3002/api/;
    # ... 同上
}

这里有个关键细节:proxy_pass 末尾带路径(/api/)会替换匹配的前缀——所以 /8gu/api/captcha → http://localhost:3002/api/captcha。如果不带路径(http://localhost:3002),则保留完整 URI(/8gu/api/captcha 原样发过去),后端就会 404。

API 路径冲突

最初前端的 axios baseURL 是 /api,请求 /api/captcha → 被 Nginx 的 location / 转发到博客的 3000 端口。博客自己有 GET /api/contact 路由,不能把整个 /api 让给 8gu。

解决方案:前端 API 基路径改为 /8gu/api,Nginx 加独立 location 处理,同时 Vite 的 dev proxy 也要同步改:

// vite.config.js
server: {
  proxy: {
    '/8gu/api': {
      target: 'http://localhost:3002',
      rewrite: path => path.replace(/^\/8gu\/api/, '/api'),
    },
  },
},
// src/lib/api.js
const api = axios.create({ baseURL: '/8gu/api' });

服务器同步:git pull 冲突

Nginx 配置改完、代码也提交推送了,上服务器拉取最新代码时遇到 git 冲突:

root@VM-0-12-ubuntu:/home/ubuntu/8gu_killer# git pull
Updating 382fd2b..4449ee8
error: Your local changes to the following files would be overwritten by merge:
        dist/index.html
        server/data.sqlite
Please commit your changes or stash them before you merge.
Aborting

两个冲突文件:dist/index.html(前端构建产物)和 server/data.sqlite(数据库)。服务器上有用户在后台加的练习题,数据库不能丢。

stash → stash pop 冲突

先 stash 再 pull:

git stash
git pull
git stash pop

git pull 顺利 Fast-forward 合并到 4449ee8,但 git stash pop 报二进制文件冲突:

warning: Cannot merge binary files: server/data.sqlite
Auto-merging server/data.sqlite
CONFLICT (content): Merge conflict in server/data.sqlite
Auto-merging dist/index.html
CONFLICT (content): Merge conflict in dist/index.html
The stash entry is kept in case you need it again.

为什么二进制文件不能自动合并

git 的 merge 算法基于 diff3——对比三个版本(共同祖先 + 两侧改动)。这对文本文件按行 diff/merge 有效,但 SQLite 数据库文件是二进制格式,git 没有"行"的概念,只能标记 "两边不一样,你选一个"。

就算本地的 data.sqlite 只是新增了 10 道题,远程改的是其他表,两边完全不冲突——git 也不知道。它只知道两个二进制文件 SHA-1 不同。

解决方案:--ours / --theirs

核心诉求:保留服务器数据库,其他同步远程。

需要理解冲突状态下 --ours 和 --theirs 的语义:

选项从谁拿当前场景
--oursHEAD(当前分支)远程的 4449ee8
--theirsstash(暂存内容)本地的改动
# 前端构建产物 → 用远程版本(重新构建就行)
git checkout --ours -- dist/index.html

# 数据库 → 保留服务器本地的数据
git checkout --theirs -- server/data.sqlite

# 标记冲突已解决
git add dist/index.html server/data.sqlite

# 清理 stash
git stash drop

最终状态:

On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
        modified:   package-lock.json
        modified:   server/data.sqlite

数据没丢,代码也同步了。最后 reload Nginx 生效。

关键收获

1. 子路径部署的三处同步修改

Vite base → React Router basename → Nginx proxy_pass,三处有一处不统一就崩。特别是 Nginx 的 proxy_pass 末尾是否带路径决定了 URI 替换行为,最容易踩坑。

2. 多应用共用域名的 API 隔离

博客有 /api/contact,8gu 有 /api/captcha——根路径下 /api 冲突。子路径化 API(/8gu/api)是最干净的解决方案。

3. 生产服务器数据库是"有状态"的

和开发环境不同,生产数据库有真实用户数据,不能被 git 覆盖。长期来看 data.sqlite 应该进 .gitignore,通过迁移脚本管理 schema 变更。