feat(deploy): 产物包部署链路与 naku 打包 CLI
Docker Compose 五服务 + Debian slim 单文件运行时;migrate 幂等门闩; naku pack/build(citty + clack);部署方案文档入档。
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
# 部署方案
|
||||
|
||||
> 母体(完整前后端 + 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`
|
||||
Reference in New Issue
Block a user