Files
new-api-key-usage/docs/design/部署方案.md
T

15 KiB
Raw Blame History

部署方案

母体(完整前后端 + 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. 总体结构

本地(打包)                              服务器(运营方)
──────────                              ────────────────
bun run pack release --yes                 /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 --yes 产出,内容固定为:

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 pack release(交互:平台 / 内容 / tar.gz);一键等价 bun run pack release --yes
脚本落点 scripts/cli.ts(citty + clack)· scripts/pack-core.ts(纯打包逻辑)
目标平台 默认 bun-linux-x64;--target= 可切 bun-linux-arm64 / bun-darwin-* / bun-windows-*(含 arm64)
构建顺序 清空输出目录 → build:web → 三次 --compile → 拷 drizzle/ →(可选)tar.gz
禁止入包 .env · 源码 · node_modules · 任何密钥
校验 打包后检查五条目都在且非空(Windows 本机编译允许 .exe 后缀);缺一即失败退出
本地试跑 pack 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 指代「约定目录」。

/opt/naku/
  docker-compose.yml
  Dockerfile
  .dockerignore
  Caddyfile
  entrypoint-pg.sh
  entrypoint-app.sh
  .env
  release/            ← 上传覆盖,不入库

5. 配置文件设计

5.1 Dockerfile(两阶段,只吃 release/)

# ---------- 应用运行时 ----------
# 用 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
# --chmod:Windows 打包 / 跨机传输常丢可执行位,缺 x 时容器内 exec 以 126 退出
COPY --chmod=755 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/ 重建镜像并重启」;
  • 单文件二进制用 COPY --chmod=755:Windows 打包 / 跨机传输常丢可执行位,缺 x 时容器内 exec 以 exit 126(Permission denied)退出,迁移与应用都不会启动;
  • 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

依赖关系:

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/443,可用 .env 的 HTTP_PORT / HTTPS_PORT 覆盖(容器内仍为 80/443)。server / engine / postgres 均不对外。
  5. 容器名固定(container_name):naku-postgres / naku-migrate / naku-server / naku-engine / naku-caddy——单副本约定下使用;服务间 DNS 仍用服务名(如 server:18456)。

5.3 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(新增入口)

loadEnv → createDb(DATABASE_URL) → runMigrations(db, MIGRATIONS_FOLDER) → 退出码
  • 复用已有 runMigrations(Drizzle migrator,按 journal 幂等:已应用的迁移号跳过);
  • 失败打日志并以非 0 退出,使 migrate 服务失败 → server/engine 不会启动(service_completed_successfully 门闩)。

6. 操作流程

6.1 首次安装

# 本地
bun run pack release --yes

# 服务器
# 将 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 日常更新(与首次同一命令)

# 本地
bun run pack release --yes
# 上传,整目录覆盖 /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 或数据后重跑固定命令
migrate exit 126? 二进制缺可执行位(Windows 打包 / 传输丢 x)。镜像内已 COPY --chmod=755 根治;宿主机侧应急 chmod +x release/naku-* 后 rebuild

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 打包/构建 CLI 与核心逻辑
packages/db/src/migrate-cli.ts 迁移 CLI 入口
package.json pack · 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)
.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 --yes。

12. 验收清单(实现后)

  • bun run pack release --yes 在 Windows 产出完整 release/(五条目齐全)
  • 首次安装按 §6.1 一次命令链可起,docker compose ps 形态符合 §6.4
  • 仅替换 release/ 后重跑固定命令,版本切换成功且数据保留(卷不重建)
  • 连续两次固定命令:第二次 migrate 不重复应用已存在迁移(日志可证)
  • 故意放一份坏 SQL:migrate 失败,server/engine 未启动
  • COOKIE_SECURE 与 HTTP/HTTPS 交叉场景符合 §8
  • 产物包内无 .env、无源码、无 node_modules