Files
book-comic-library/docs/superpowers/specs/2026-09-07-docker-deploy-spec-design.md

115 lines
9.0 KiB
Markdown
Raw Permalink 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.
# 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`;`smoke.sh` 里依赖旧 `./library:/data/books` 绑定的宿主路径 `library/smoke-books`(建目录与删除断言两处)同步改为 `deploy/api/storage/smoke-books`。
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` 全绿。