From e563c88913026371cc2c5da2aa674c7ded94934d Mon Sep 17 00:00:00 2001 From: XingfenD Date: Mon, 7 Sep 2026 19:36:34 +0800 Subject: [PATCH] docs(spec): docker deployment conformance redesign (dev/release compose, prepare.sh, shared volumes, storage path) --- .../2026-09-07-docker-deploy-spec-design.md | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-07-docker-deploy-spec-design.md diff --git a/docs/superpowers/specs/2026-09-07-docker-deploy-spec-design.md b/docs/superpowers/specs/2026-09-07-docker-deploy-spec-design.md new file mode 100644 index 0000000..30d3be0 --- /dev/null +++ b/docs/superpowers/specs/2026-09-07-docker-deploy-spec-design.md @@ -0,0 +1,114 @@ +# Docker 部署规范化改写 — 设计文档 + +日期:2026-09-07 · 状态:已确认(用户批准) · 范围:仅部署形态,不改业务架构 + +## 1. 背景与目标 + +现有部署(根 `docker-compose.yml` + `deploy/docker-compose.dev.yml`)与团队 Docker 部署规范不一致。本改造把 booklib 重写为规范形态: + +- dev = `deploy/docker-compose.dev.yml`,release = `deploy/docker-compose.yml`(根 compose 删除) +- 两模式共享 named volumes,`down -v` 才清数据 +- 配置由 `prepare.sh` 从模板渲染后 `:ro` 挂载进容器 +- dev 源码热挂载 + `target: dev` 镜像 + delve :2345;release 用 `*.prod` Dockerfile(`target: runner`)编译产物,不挂源码 +- 文件存储挂 `deploy/api/storage`(对齐 `deploy/{service_name}/storage` 约定) + +**非目标**:不引入 rabbitmq/consul/minio/ES,不拆分 worker,不改业务代码(除 §7 列出的两处适配)。 + +## 2. 目录布局 + +``` +deploy/ + docker-compose.yml # release + docker-compose.dev.yml # dev(重写:四服务全容器化) + prepare.sh # 渲染配置,up 之前必须执行 + .env.example # 从仓库根迁入;本地 .env 同位置(项目目录=deploy/,插值才生效) + templates/ + nginx.conf.tpl # nginx 主配置(events/http、include conf.d、日志与 pid 路径) + default.conf.tpl # server 块(CSP、/api 反代、SPA fallback、resolver 占位行) + redis.conf.tpl # maxmemory / policy + nginx/nginx.conf # 渲染产物(gitignore) + nginx/conf.d/default.conf # 渲染产物(gitignore) + redis/redis.conf # 渲染产物(gitignore) + logs/nginx/ # prepare.sh 创建;宿主机收集 nginx access/error log(gitignore) + api/storage/ # 书库+缓存绑定挂载(gitignore,保留 .gitkeep) + Dockerfile.api.dev # 多阶段,target: dev(golang + delve) + Dockerfile.api.prod # 多阶段,target: runner(静态二进制) + Dockerfile.web.dev # 多阶段,target: dev(node,跑 vite) + Dockerfile.web.prod # 多阶段,target: runner(构建 SPA + nginx) + entrypoint-resolver.sh # 保留并调整:见 §5 +``` + +删除:根 `docker-compose.yml`、根 `.env.example`、旧 `deploy/Dockerfile.api`、旧 `deploy/Dockerfile.web`、旧 `deploy/nginx.conf`。`library/` 目录废弃(当前为空且 gitignore)。 + +## 3. 项目名与共享卷 + +- 两个 compose 顶层显式 `name: booklib`,与运行目录解耦。 +- 卷:`postgres_data`、`redis_data` → 实际名 `booklib_postgres_data`、`booklib_redis_data`;两文件完全同名,dev/release 看到同一份数据。仅 `down -v`(rebuild/clear)清除。 +- redis 按规范挂 `redis_data` 到 `/data`(原「redis 不落盘」设计让位于规范;只是多持久化一份可再生缓存,无功能副作用)。 +- 旧卷迁移(README 记录一次性命令): + `docker run --rm -v book-comic-library_pgdata:/from -v booklib_postgres_data:/to alpine cp -a /from/. /to/` + 旧 `cache` 卷为封面/解压缓存,可直接丢弃(自动重建)。 + +## 4. dev 模式(`deploy/docker-compose.dev.yml`) + +| 服务 | 镜像/build | 挂载 | 端口(宿主:容器) | 启动 | +|---|---|---|---|---| +| postgres | postgres:16-alpine | volume `postgres_data` | 5432:5432 | 默认 | +| redis | redis:7-alpine | `./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro`、`redis_data:/data` | 6379:6379 | `redis-server /usr/local/etc/redis/redis.conf`(移除命令行 maxmemory) | +| api | `Dockerfile.api.dev` target `dev` | `../backend:/app`、`./api/storage:/data/books` | 2345:2345(delve) | `go mod download && dlv debug ./cmd/server --headless --listen=0.0.0.0:2345 --api-version=2 --log` | +| web | `Dockerfile.web.dev` target `dev` | `../web:/app` + 匿名卷 `/app/node_modules`(镜像内依赖不被覆盖) | 5173:5173 | `npm run dev -- --host 0.0.0.0` | + +- api env:`DATABASE_URL=postgres://lib:lib@postgres:5432/lib?sslmode=disable`、`REDIS_URL=redis://redis:6379`、`BOOKS_DIR=/data/books`、`CACHE_DIR=/data/books/cache`、`JWT_SECRET/ADMIN_USER/ADMIN_PASSWORD/SCAN_INTERVAL_SEC` 自 `.env` 插值。 +- 启动门控:api `depends_on` postgres `service_healthy` + redis `service_started`;web 不依赖 api 健康(vite 反代容忍后端重启)。 +- 热更新语义:改 Go/TS 源码 → `docker compose ... restart api|web` 即生效(go run/dlv 重编译、vite 本身 HMR),全程无需 rebuild 镜像。 +- dev 不起 nginx(vite 代理承担 `/api`)。因此 nginx 配置/log 绑定挂载只在 release 出现——这是对规范「两模式都挂 nginx 配置」的有意偏差:dev 无 nginx 进程,挂之无意义。 +- 删容器用 `down`,不用 `rm`,避免匿名 node_modules 卷成为孤儿。 + +## 5. release 模式(`deploy/docker-compose.yml`) + +- web:`Dockerfile.web.prod` target `runner`;宿主端口 `${WEB_PORT:-8080}:80`;挂载 + `./nginx/nginx.conf:/etc/booklib/nginx.conf:ro`、`./nginx/conf.d/default.conf:/etc/booklib/default.conf:ro`、`./logs/nginx:/var/log/nginx`。 + 配置挂到 `/etc/booklib/` 暂存而非直接挂 `/etc/nginx/conf.d/`,因为 `:ro` 绑定挂载下 entrypoint 的 sed 无法原地改写。 +- `entrypoint-resolver.sh` 调整:启动时 `cp /etc/booklib/default.conf → /etc/nginx/conf.d/default.conf`,再按 `/etc/resolv.conf` 首个 nameserver sed 注入 resolver,然后 exec nginx。镜像不再 COPY baked 配置(`Dockerfile.web.prod` 移除该 COPY;若忘跑 prepare.sh,文件缺失会让 compose 启动即报错,符合规范「stale/缺失 = 忘了重跑 prepare」)。 +- api:`Dockerfile.api.prod` target `runner`;无源码挂载、无配置挂载(本项目 api 配置全部来自 env,符合「Go 服务只挂配置文件」——无可挂项即为不挂);`./api/storage:/data/books` 与 dev 同路径同数据;`BOOKS_DIR=/data/books`、`CACHE_DIR=/data/books/cache`。 +- postgres/redis:不暴露宿主端口,仅容器网络;redis 同样挂渲染 `redis.conf` + `redis_data`。 +- 渲染产物 bind mount 集合与规范对齐:nginx 两份 + redis.conf + logs/nginx。 + +## 6. prepare.sh 与模板变量 + +`deploy/prepare.sh`(`set -eu`,在脚本自身目录执行): + +1. 若存在 `deploy/.env` 则逐行解析 `KEY=VALUE` 注入渲染环境;未定义变量用下表默认值兜底。 +2. `mkdir -p nginx/conf.d redis logs/nginx api/storage`。 +3. 用 sed 把模板中 `{{VAR}}` 替换为实际值,渲染到对应产物路径(幂等,可反复执行)。 + +| 变量 | 默认值 | 用途 | +|---|---|---| +| `WEB_PORT` | 8080 | release 宿主端口 | +| `NGINX_CLIENT_MAX_BODY_SIZE` | 200m | default.conf.tpl | +| `REDIS_MAXMEMORY` | 128mb | redis.conf.tpl | +| `REDIS_MAXMEMORY_POLICY` | allkeys-lru | redis.conf.tpl | +| `DELVE_PORT` | 2345 | dev compose 的 delve 宿主端口(非模板变量,供 `${DELVE_PORT:-2345}` 插值) | + +现有 `.env` 键(JWT_SECRET/ADMIN_USER/ADMIN_PASSWORD/SCAN_INTERVAL_SEC)不变;新增上表键写入 `.env.example`。 + +## 7. 周边代码改动(最小集) + +1. `web/vite.config.ts`:proxy target 改为 `process.env.VITE_PROXY_TARGET ?? "http://localhost:8080"`;dev compose 给 web 服务注入 `VITE_PROXY_TARGET=http://api:8080`。宿主机跑 vite 的旧习惯不受影响。 +2. `scripts/smoke.sh` / `scripts/smoke-web.sh`:`. ./.env` 改为优先 `deploy/.env`、回退根 `.env`。 +3. `.gitignore`:删 `library/`,增 `deploy/.env`、`deploy/nginx/`、`deploy/redis/redis.conf`、`deploy/logs/`、`deploy/api/storage/*`(`!.gitkeep`)。 +4. `README.md`:dev/release 命令全部重写(`docker compose -f deploy/docker-compose.yml|docker-compose.dev.yml`,先 `deploy/prepare.sh`),旧卷迁移说明,dev 调试(delve :2345 / IDE attach 示例)。 + +## 8. 错误处理 + +- 未跑 prepare.sh 就 `up`:compose 绑定挂载源不存在 → Docker 直接报错(不是静默空目录);`prepare.sh` 本身缺 `.env` 不报错(用默认值渲染),缺模板则报错退出。 +- api 连不上 PG:healthcheck 门控 + 后端已有启动失败即退出的行为不变。 +- delve 端口冲突:`2345` 可经 `.env` 变量 `DELVE_PORT`(默认 2345)覆盖。 + +## 9. 验收 + +1. `deploy/prepare.sh && docker compose -f deploy/docker-compose.yml up -d --build` → `bash scripts/smoke.sh` 与 `bash scripts/smoke-web.sh` 全绿(端口语义与现在一致:宿主 8080)。 +2. `docker compose -f deploy/docker-compose.dev.yml up -d --build` → `curl localhost:5173` 返回 vite 页面、`curl localhost:5173/api/healthz` 经代理 200;宿主 `nc -z localhost 2345` 通。 +3. 数据共享验证:dev 建库上传 → dev `down` → release `up` → 数据可见;`docker volume ls | grep booklib_` 两卷名一致。 +4. 规范符合性核对表逐项打勾(挂载、target、named volume、prepare 产物、存储路径)。 +5. 回归:`cd backend && go vet ./... && go test -p 1 -count=1 ./...`、`cd web && npm run check` 全绿。