# 部署方案 > 母体(完整前后端 + 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`