Files
new-api-key-usage/docs/design/部署方案.md
T
skycurtain 905d4ce77e feat(deploy): 产物包部署链路与 naku 打包 CLI
Docker Compose 五服务 + Debian slim 单文件运行时;migrate 幂等门闩;
naku pack/build(citty + clack);部署方案文档入档。
2026-10-13 00:56:43 +08:00

328 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署方案
> 母体(完整前后端 + PostgreSQL)的交付与更新方案:产物包形态、服务器目录约定、容器编排、迁移时机、安全边界与固定操作流程。**范围**:只定「东西怎么装到运营方服务器、怎么更新」;不改业务语义,不覆盖 R13 的 CFW 抽取版部署面。设计对齐作者现场运维心智——**本地产物 → 传到服务器约定目录 → 一条固定命令 → 看容器状态**。
| 项目 | 内容 |
|------|------|
| 状态 | 已实现(定稿 2026-10-12) |
| 关联 | `技术选型.md`(Bun / Hono / Drizzle / Vue)· `同步引擎设计.md`(独立 `apps/engine` 进程)· `需求清单.md` R13/R14 · `users表字段设计.md`(部署期初始化)· `README.md` |
| 产出 | 产物包布局 · 服务器目录 · Dockerfile / Compose / Caddy 要点 · 打包与更新流程 · 迁移与安全约定 |
---
## 1. 设计约束(来自现场经验与项目事实)
1. **操作者心智**(作者工作流,优先对齐):
- 前端:本地打包 `dist/` → 放到服务器指定目录 → 执行固定命令 → `docker ps` 看容器全绿;
- 后端:拿到 `jar` 类产物 → 上传指定目录 → 同一固定命令 → 看容器。
2. **仓库事实**:`apps/server` 与 `apps/engine` 两个独立进程;前端为静态 `dist`(server 不托管静态);数据在 PostgreSQL;配置全走 env;`packages/db` 已有按 journal 的幂等 `runMigrations`。
3. **能力边界**:作者是前端,Docker 不娴熟;服务器侧只需 Docker,不要求 Bun / Node / 源码构建能力。
4. **协作约定**:交付物保证「一条命令链」可部署(AGENTS);密钥 `SK_KEY_ENCRYPTION_SECRET` 丢失 = 已登记 key 全部无法解密。
由此收敛出的形态一句话:**常驻 Docker 编排 + 每次更新只替换产物包 + 一条 compose 命令完成重建/重启/迁表。**
---
## 2. 总体结构
```text
本地(打包) 服务器(运营方)
────────── ────────────────
bun run pack:release /opt/naku/ ← 约定目录
├─ 前端 dist/ ├─ docker-compose.yml
├─ naku-server(linux 单文件) ├─ Dockerfile
├─ naku-engine(linux 单文件) ├─ .dockerignore
├─ naku-migrate(linux 单文件) ├─ Caddyfile
└─ drizzle/(迁移 SQL + journal) ├─ entrypoint-pg.sh
│ ├─ entrypoint-app.sh
▼ ├─ .env ← 密钥在此,不进包
release/ ──── 上传覆盖 ──────────► └─ release/ ← 每次更新整目录覆盖
固定命令: docker compose up -d --build
验收: docker compose ps
```
| 角色 | 职责 | 谁改 |
|------|------|------|
| 常驻编排(compose / Dockerfile / Caddyfile / entrypoint-*.sh / .env) | 描述系统怎么跑 | 装机时放好;改架构时才动 |
| 产物包 `release/` | 当前要跑的版本 | 每次发版整目录替换 |
首次安装与日常更新**同一套命令**;差别只是首次多一步「把常驻编排放到服务器」。
---
## 3. 产物包 `release/`
本地 `bun run pack:release` 产出,内容固定为:
```text
release/
dist/ # apps/web 构建产物(index.html · assets/…)
naku-server # Bun --compile,目标平台 linux-x64
naku-engine # 同上
naku-migrate # 同上;只跑迁移后退出
drizzle/ # packages/db/drizzle/ 的 SQL + meta/_journal.json
```
### 3.1 打包约定
| 项 | 约定 |
|----|------|
| 命令入口 | 优先 `bun run naku pack`(交互:平台 / 内容 / tar.gz);一键等价 `bun run pack:release` 或 `naku pack --yes` |
| 脚本落点 | `scripts/cli.ts`(citty + clack)· `scripts/pack-core.ts`(纯打包逻辑)· `scripts/pack-release.ts`(非交互兼容入口) |
| 目标平台 | 默认 `bun-linux-x64`;`--target=` 可切 `bun-linux-arm64` / `bun-darwin-*` / `bun-windows-*`(含 arm64) |
| 构建顺序 | 清空输出目录 → `build:web` → 三次 `--compile` → 拷 `drizzle/` →(可选)tar.gz |
| 禁止入包 | `.env` · 源码 · `node_modules` · 任何密钥 |
| 校验 | 打包后检查五条目都在且非空(Windows 本机编译允许 `.exe` 后缀);缺一即失败退出 |
| 本地试跑 | `naku build` / `build:server` / `build:engine` → `bin/`(当前平台),与部署包 `release/` 分离 |
### 3.2 为何是单文件而不是「服务器上构建」或「上传源码」
- 对齐 `jar` 心智:产物即交付物,服务器零构建依赖;
- 更新体积小、失败面小(不跑 `bun install` / `vite build`);
- 运行时镜像只需 Debian slim + `ca-certificates` + `libstdc++6`,不需要 Bun。
代价:目标平台写在打包脚本里;换 CPU 架构需重新打包(在脚本参数上留出口即可)。
---
## 4. 服务器目录(`/opt/naku/`)
路径可改,下文以 `/opt/naku` 指代「约定目录」。
```text
/opt/naku/
docker-compose.yml
Dockerfile
.dockerignore
Caddyfile
entrypoint-pg.sh
entrypoint-app.sh
.env
release/ ← 上传覆盖,不入库
```
---
## 5. 配置文件设计
### 5.1 `Dockerfile`(两阶段,只吃 `release/`)
```dockerfile
# ---------- 应用运行时 ----------
# 用 Debian slim:与 Bun --compile(glibc)同源,兼容面更稳;
# CA 与 libstdc++ 显式安装,不依赖 slim 是否自带
FROM debian:bookworm-slim AS app
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libstdc++6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY release/naku-server release/naku-engine release/naku-migrate /app/
COPY release/drizzle /app/drizzle
ENV MIGRATIONS_FOLDER=/app/drizzle
CMD ["/app/naku-server"]
# ---------- 网关(Caddy + 前端静态) ----------
FROM caddy:2-alpine AS proxy
COPY Caddyfile /etc/caddy/Caddyfile
COPY release/dist /srv
```
要点:
- **构建期不编译业务代码**,只装配产物;`docker compose up -d --build` 的语义是「用当前 `release/` 重建镜像并重启」;
- `app` 阶段一个镜像供 `server` / `engine` / `migrate` 三用,靠 compose 的 `command` 区分;
- 应用运行时选 **`debian:bookworm-slim`**(glibc,对齐 Bun `--compile` 目标);`ca-certificates`(HTTPS 证书校验)与 `libstdc++6`(单文件 C++ 运行时)显式安装——两个都不指望镜像自带;
- 网关仍用官方 `caddy:2-alpine`(与应用基底无关,体积友好);
- PostgreSQL 使用官方镜像,不进本 Dockerfile。
### 5.2 `docker-compose.yml`(五服务)
| 服务 | 来源 | command | 说明 |
|------|------|---------|------|
| `postgres` | `postgres:17-alpine` | 官方入口 | 卷 `pgdata`;`healthcheck` 用 `pg_isready` |
| `migrate` | `target: app` | `/app/naku-migrate` | **一次性**:`restart: "no"`;迁完退出 |
| `server` | `target: app` | `/app/naku-server` | 管理面 / 成员视图 API |
| `engine` | `target: app` | `/app/naku-engine` | 轮询同步 |
| `caddy` | `target: proxy` | 官方入口 | **唯一对外**:`80` / `443` |
依赖关系:
```text
postgres (healthy) ──► migrate (completed successfully) ──► server ──► caddy
└──────► engine
```
要点:
1. **`migrate` 独立服务**,而不是塞进 server 启动:语义是「部署时迁表」,避免 server/engine 双进程抢迁;`depends_on: service_completed_successfully` 保证先迁后跑。
2. **环境变量**:`DATABASE_URL` · `SK_KEY_ENCRYPTION_SECRET` · `NEWAPI_BASE` · `COOKIE_SECURE` · `LOG_LEVEL` · `SYNC_INTERVAL_MS` · 可选 `ADMIN_INIT_*`——均从 `.env` 注入 `server` / `engine`(`migrate` 只需要 `DATABASE_URL`)。
3. **`DATABASE_URL` 单源**:用户 / 密码 / 库名都写在这一串里;`entrypoint-pg.sh` 解析出 `POSTGRES_USER/PASSWORD/DB` 喂给官方库镜像,`entrypoint-app.sh` 把主机改写为 compose 服务名 `postgres`。开发与部署共用同一份 `.env` 语义。
4. 端口:仅 `caddy` 映射 `80:80`、`443:443`;`server` / `engine` / `postgres` 均不对外。
5. **容器名固定**(`container_name`):`naku-postgres` / `naku-migrate` / `naku-server` / `naku-engine` / `naku-caddy`——单副本约定下使用;服务间 DNS 仍用服务名(如 `server:18456`)。
### 5.3 `Caddyfile`
```caddyfile
# 站点地址按部署环境改:
# 本地/内网 HTTP → :80
# 公网 HTTPS → your.domain.example(Caddy 自动签证书)
:80 {
root * /srv
handle /api/* {
reverse_proxy server:18456
}
handle {
try_files {path} /index.html
file_server
}
}
```
与 `API契约-公共约定` 的命名空间约定一致:`/api/*` 归接口,其余归 SPA(非 `/api` 回退 `index.html`)。
### 5.4 `entrypoint-pg.sh` / `entrypoint-app.sh`
| 脚本 | 职责 |
|------|------|
| `entrypoint-pg.sh` | 从 `DATABASE_URL` 拆出 `POSTGRES_USER` / `POSTGRES_PASSWORD` / `POSTGRES_DB`,`exec` 官方 `docker-entrypoint.sh postgres` |
| `entrypoint-app.sh` | 将 `DATABASE_URL` 的主机段改写为 `postgres`(保留端口与路径),再 `exec "$@"` 启动传入的二进制 |
二者属**常驻编排**,打进镜像或只读挂载皆可(建议只读挂载,改脚本不用重建镜像)。
### 5.5 `packages/db/src/migrate-cli.ts`(新增入口)
```text
loadEnv → createDb(DATABASE_URL) → runMigrations(db, MIGRATIONS_FOLDER) → 退出码
```
- 复用已有 `runMigrations`(Drizzle migrator,**按 journal 幂等**:已应用的迁移号跳过);
- 失败打日志并以非 0 退出,使 `migrate` 服务失败 → `server`/`engine` 不会启动(`service_completed_successfully` 门闩)。
---
## 6. 操作流程
### 6.1 首次安装
```bash
# 本地
bun run pack:release
# 服务器
# 将 docker-compose.yml · Dockerfile · .dockerignore · Caddyfile · entrypoint-*.sh 放入 /opt/naku/
# 将 release/ 上传到 /opt/naku/release/
cp .env.example /opt/naku/.env # 再编辑填写
chmod 600 /opt/naku/.env
cd /opt/naku
docker compose up -d --build
docker compose ps
```
### 6.2 日常更新(与首次同一命令)
```bash
# 本地
bun run pack:release
# 上传,整目录覆盖 /opt/naku/release/
# 服务器(固定命令)
cd /opt/naku
docker compose up -d --build
docker compose ps
```
- 有表变更时,新 `release/drizzle/` 自动带新 SQL,`migrate` 启动时补跑——无需手工;
- 观察构建/迁移日志:`docker compose logs migrate`;同步:`docker compose logs -f engine`。
### 6.3 HTTPS(可选)
1. `Caddyfile` 站点行 `:80` → 域名;
2. `.env` 中 `COOKIE_SECURE=true`;
3. 重新执行固定命令。
### 6.4 验收标准
- `docker compose ps`:`postgres` healthy,`server` / `engine` / `caddy` running,`migrate` exited(0);
- 浏览器打开站点可见页面;`/api/docs` 可访问;
- `docker compose logs engine` 出现就绪/轮次日志(已配置 key 时)。
---
## 7. 迁移策略(裁决)
| 问题 | 裁决 |
|------|------|
| 会不会「多灌几次」? | **不会。** 使用 Drizzle `migrate()`(journal 表),重复执行只应用未跑过的迁移号 |
| 谁来跑? | compose 一次性服务 `migrate`(`naku-migrate` 单文件),不随 server/engine 长驻 |
| 何时跑? | 每次固定命令(含首次)启动/更新时 |
| 不用什么? | **不用 `db:push` 进部署链**——它是 schema diff 硬推,不适合生产更新命令 |
| 失败怎么办? | `migrate` 非 0 退出 → `server`/`engine` 不启动;修复 SQL 或数据后重跑固定命令 |
---
## 8. 安全约定
| 点 | 约定 |
|----|------|
| `.env` 位置 | 只在服务器约定目录;**不进产物包、不进 git** |
| 权限 | `chmod 600 .env`,仅部署用户可读 |
| `SK_KEY_ENCRYPTION_SECRET` | 生成一次、入库后不可更换;**与数据库同等备份**(`.env` 必须备份) |
| 对外暴露 | 仅 Caddy 的 80/443;库与应用进程不映射公网端口 |
| `COOKIE_SECURE` | 必须与访问方式匹配:HTTP + `true` 会「登录后立刻 401」;HTTPS/域名用 `true` |
| 日志 | 不打印密钥与完整 `DATABASE_URL`(含密码) |
| 范围说明 | 单运营方内部服务器场景;不上 vault/KMS。若未来有多租户或公有多环境,再升档 |
---
## 9. 与开发方式的关系
| | 开发 | 部署 |
|--|------|------|
| 进程 | `bun run dev:server` / `dev:engine` / `dev:web` | compose:`server` / `engine` / `caddy` |
| 库 | 任意 PostgreSQL,`DATABASE_URL` 指向即可 | compose 内 `postgres`,同一 `DATABASE_URL` 语义(主机由 entrypoint 改写) |
| 建表 | `bun run db:push`(开发期) | `migrate` 服务(生产幂等迁移) |
| 前端 | Vite dev server | `release/dist` 由 Caddy 托管 |
| 配置 | `.env` | 服务器 `.env` |
开发**不依赖**本方案的 Docker 配置;部署也**不依赖**本机 Bun。
---
## 10. 文件改动清单
| 文件 | 动作 |
|------|------|
| `scripts/cli.ts` · `scripts/pack-core.ts` · `scripts/pack-release.ts` | 打包/构建 CLI 与核心逻辑 |
| `packages/db/src/migrate-cli.ts` | 迁移 CLI 入口 |
| `package.json` | `naku` · `pack:release` · `build:server` · `build:engine` |
| `Dockerfile` · `docker-compose.yml` · `.dockerignore` | 两阶段镜像 · 五服务(含 `container_name`) |
| `Caddyfile` · `entrypoint-pg.sh` · `entrypoint-app.sh` | 与 compose 同级平铺 |
| `README.md` | 「部署」章与文档地图 |
| `AGENTS.md` | 命令表(pack / build / naku) |
| `.env.example` | 部署语义注释 |
---
## 11. 已知取舍(否决时改这里)
1. **`migrate` 独立单文件**:多一个产物,换来「先迁后跑、失败不启」的清晰门闩。若嫌多,可改为 server 启动时迁(仍幂等),但失去 migrate 失败阻断 engine 的不对称控制。
2. **目标平台写死 linux-x64**:匹配常见服务器;ARM 用打包参数切换,不为此引入交叉编译矩阵。
3. **`DATABASE_URL` 单源 + 两个 entrypoint**:少一处密码不一致;多两个常驻小 shell。若偏好 compose 原生 `POSTGRES_*`,需接受密码在 `.env` 写两处(或 `DATABASE_URL` 仅应用用)。
4. **产物包而非源码包**:服务器零构建;代价是发版必须在本地(或 CI)跑 `pack:release`。
---
## 12. 验收清单(实现后)
- [ ] `bun run naku pack --yes`(或 `pack:release`)在 Windows 产出完整 `release/`(五条目齐全)
- [ ] 首次安装按 §6.1 一次命令链可起,`docker compose ps` 形态符合 §6.4
- [ ] 仅替换 `release/` 后重跑固定命令,版本切换成功且数据保留(卷不重建)
- [ ] 连续两次固定命令:第二次 `migrate` 不重复应用已存在迁移(日志可证)
- [ ] 故意放一份坏 SQL:`migrate` 失败,`server`/`engine` 未启动
- [ ] `COOKIE_SECURE` 与 HTTP/HTTPS 交叉场景符合 §8
- [ ] 产物包内无 `.env`、无源码、无 `node_modules`