From 542eeefdbdca1446665def9aebf39152cef02bf3 Mon Sep 17 00:00:00 2001 From: dela Date: Sun, 24 May 2026 21:18:19 +0800 Subject: [PATCH] deploy: harden compose harness --- .dockerignore | 61 +++++++++++++++++++++++++++++++++++++++++++ .env.example | 7 ++--- AGENTS.md | 4 +++ Makefile | 4 +++ README.md | 5 ++-- ai/harness-health.md | 6 ++--- ai/risk-guardrails.md | 26 ++++++++++++++++++ ai/task-templates.md | 2 +- docker-compose.yml | 8 +++--- 9 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9542327 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,61 @@ +# cryptoHermes .dockerignore +# +# 作用:缩小 docker build context、防止敏感文件(.env)进入 builder layer。 +# Docker 的 .dockerignore 语法和 .gitignore 类似,但**作用域是 build context**, +# 不是 git 工作区——所以即使某文件没在 .gitignore 里,只要它出现在 COPY . +# 的源目录,就会被发到 daemon 并可能缓存到远端 registry。 +# +# 修改本文件后跑 `docker build --progress=plain .` 看 "transferring context" +# 一行的大小变化,确认未泄漏。 + +# ---- 密钥 / 环境变量 ---- +# 高危:.env 内含 POSTGRES_PASSWORD、可能含代理凭据。绝不能进 build context。 +# .env.example 也排掉——镜像运行时不读它(环境变量由 docker compose 注入)。 +.env +.env.* + +# ---- Git / 工具元数据 ---- +.git +.gitignore +.gitattributes + +# ---- 本机构建产物 ---- +bin/ +*.exe +*.test + +# ---- 测试覆盖率产物 ---- +*.out +coverage.* + +# ---- 运行时数据(容器外挂载,不进镜像)---- +logs/ + +# ---- 编辑器 / IDE ---- +.vscode/ +.idea/ +.claude/ +*.swp +.DS_Store +Thumbs.db + +# ---- Docker 自身(镜像里用不到)---- +Dockerfile +.dockerignore +docker-compose.yml +docker-compose.*.yml + +# ---- 文档与工程治理(不需要进镜像)---- +README.md +AGENTS.md +docs/ +ai/ + +# ---- Migration 文件 ---- +# 镜像里不需要:migrate init container 走宿主机 bind mount 读 ./migrations, +# app 二进制本身不解析 migration。 +# 如果未来要把 migration 嵌进 app 做 self-migrate,再删掉这一行。 +migrations/ + +# ---- Go ---- +vendor/ diff --git a/.env.example b/.env.example index 50632a3..e08397e 100644 --- a/.env.example +++ b/.env.example @@ -3,14 +3,15 @@ # .env 不入库(.gitignore 已忽略);.env.example 入库给团队作模板 # ---- Postgres ---- -# 强密码必须在生产环境覆盖 +# 必填:复制为 .env 后必须填写强密码;留空时 docker compose 会拒绝启动。 POSTGRES_USER=postgres -POSTGRES_PASSWORD=postgres +POSTGRES_PASSWORD= POSTGRES_DB=hermes_market POSTGRES_PORT=5432 # ---- App ---- -APP_PORT=8080 +# 宿主机暴露端口;容器内 app 固定监听 8080。 +APP_HOST_PORT=8080 APP_ENV=production # ---- Binance ---- diff --git a/AGENTS.md b/AGENTS.md index 8b80ac6..3042138 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,9 @@ go test ./internal//... # 全部测试 make test # 等价于 go test ./... +# 生产部署 / 架构守卫 +make guard + # 启动(需要先 make docker-up && make migrate-up) make run ``` @@ -122,6 +125,7 @@ grep -ri "apikey\|x-mbx-apikey\|hmac\|secret_key\|api_secret" --include="*.go" . 4. **Rate limit**:默认 20 req/s(`config.Binance.RPS`)。新增 collector 任务前估算 weight 预算(Binance 2400 weight/min IP 上限)。 5. **国内网络**:`fapi.binance.com` 直连不稳定,通过 `HTTPS_PROXY` 环境变量挂代理。 6. **Upsert 幂等**:5 张表的主键都包含时间维度,所有 repo 的 `UpsertMany` 使用 `ON CONFLICT DO UPDATE`,可安全重复执行。 +7. **Compose 部署约束**:`.env.example` 不给默认 DB 密码;宿主机端口只改 `APP_HOST_PORT`;容器内 `APP_PORT` 固定 8080;migration 必须在 app 前完成;`.dockerignore` 必须排除 `.env` / `.env.*`。 --- diff --git a/Makefile b/Makefile index 9fc8fd3..724dab3 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,10 @@ guard: @echo "G12.2 (indicator no repo)..." && ! grep -q "internal/repo" internal/usecase/indicator.go @echo "G12.3 (indicator no DB)..." && ! grep -qE "binance|postgres|pgx" internal/usecase/indicator.go @echo "G12.4 (entity no float)..." && ! grep -rqn --include="*.go" "float64\|float32" internal/entity + @echo "G13.1 (compose requires password)..." && grep -qx 'POSTGRES_PASSWORD=' .env.example + @echo "G13.2 (compose fixed app port)..." && grep -q 'APP_HOST_PORT' docker-compose.yml && grep -q 'APP_PORT: "8080"' docker-compose.yml && ! grep -q '$${APP_PORT' docker-compose.yml + @echo "G13.3 (compose migrations gated)..." && grep -q 'service_healthy' docker-compose.yml && grep -q 'service_completed_successfully' docker-compose.yml + @echo "G13.4 (dockerignore protects env)..." && grep -qx '.env' .dockerignore && grep -qx '.env.\*' .dockerignore @echo "all guards green" docker-up: diff --git a/README.md b/README.md index db6c1a2..7fb7963 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ curl -s 'localhost:8080/v1/market/context?symbol=BTCUSDT' | jq 'keys' ```bash # 1. 准备环境变量 cp .env.example .env -# 编辑 .env,至少修改 POSTGRES_PASSWORD(不修改 compose 直接拒绝启动) +# 编辑 .env,必须填写 POSTGRES_PASSWORD(留空时 compose 直接拒绝启动) # 国内机器还要填 HTTP_PROXY / HTTPS_PROXY # 2. 启动(自动建表) @@ -231,7 +231,8 @@ collector: |---|---| | `CONFIG_PATH` | 配置文件路径,默认 `config/config.yml` | | `POSTGRES_DSN` | 覆盖 yaml 中的 DSN(Docker Compose 里就是这么干的) | -| `APP_PORT` | HTTP 端口 | +| `APP_PORT` | HTTP 端口;compose 部署中容器内固定为 8080 | +| `APP_HOST_PORT` | docker compose 暴露到宿主机的 HTTP 端口 | | `HTTPS_PROXY` | 走代理访问 Binance | --- diff --git a/ai/harness-health.md b/ai/harness-health.md index d4bc684..5264efa 100644 --- a/ai/harness-health.md +++ b/ai/harness-health.md @@ -15,7 +15,7 @@ | 验证命令可跑 | strong | Makefile 全部命令在 README + AGENTS.md 双重列出 | | 快速反馈环路 | strong | `go build ./...` < 2s,`go vet ./...` < 1s,单包测试支持 | | 任务契约质量 | partial | 模板已写(`ai/task-templates.md`),但还没有真实任务用过 | -| 守卫规则清晰度 | strong | 11 条 G1-G11,每条都有可机械验证命令 | +| 守卫规则清晰度 | strong | G1-G13 覆盖交易硬边界、精度、架构、指标和 compose 部署路径,关键规则进入 `make guard` | | 架构决策耐久度 | strong | ADR 0001 记录 9 条核心决策;其他 ADR 等触发 | | Spec / test-plan | partial | 还没有 specs/,因为 v1 没有需要 spec 的任务;v2 新数据源时必填 | | 失败反馈环路 | partial | AGENTS.md §10 写了升级时机,但还没有 `ai/risk-guardrails.md` 改动史可参考 | @@ -31,7 +31,7 @@ ## 已具备的工程基础 - **3 层 AI 文档系统**:`AGENTS.md`(速查)→ `ai/project-map.md`(结构)→ `ai/adr/`(决策)。`docs/dev.md` 作为最详细的设计源头单独存在。 -- **可机械验证的守卫**:G1-G10 全部给了 grep / make 命令,没有靠 review 的软规则。 +- **可机械验证的守卫**:G1-G13 给了 grep / make / compose 验证命令,关键规则进入 `make guard`。 - **唯一的组合根**:`internal/app/app.go`,DI 一处可见。 - **接口边界文件**:`internal/usecase/ports.go` 是唯一定义对外接口的位置,新 agent 找接口去这里。 - **强约束 grep 在 AGENTS.md §6**:每次 PR 都可以直接跑。 @@ -95,7 +95,7 @@ AGENTS.md ← AI agent 速查 README.md ← 用户向使用文档 docs/dev.md ← 完整设计文档(源头) ai/project-map.md ← 仓库结构与扩展点 -ai/risk-guardrails.md ← G1-G10 守卫规则 +ai/risk-guardrails.md ← G1-G13 守卫规则 ai/adr/0001-architecture-foundations.md ← 9 条架构基础决策 ai/task-templates.md ← 6 种任务模板 ai/harness-health.md ← 本文件 diff --git a/ai/risk-guardrails.md b/ai/risk-guardrails.md index 08d0bad..ce5de63 100644 --- a/ai/risk-guardrails.md +++ b/ai/risk-guardrails.md @@ -222,6 +222,32 @@ grep -rn --include="*.go" "float64\|float32" internal/entity --- +## G13. docker compose 部署路径不可裸奔 + +**规则**:`docker compose up -d` 的生产/测试路径必须满足: + +- Postgres 密码从 `.env` 注入,`.env.example` 只能留空占位,不能给 `postgres` 这类弱默认值。 +- `.dockerignore` 必须排除 `.env` 和 `.env.*`,防止 `Dockerfile` 的 `COPY . .` 把 secrets 送进 build context / builder layer。 +- app 容器内端口固定为 8080;宿主机暴露端口只通过 `APP_HOST_PORT` 调整。不要在 compose 中用 `${APP_PORT}` 同时控制容器内监听和宿主机映射。 +- app 必须等待 Postgres healthcheck 通过、migration init container 成功退出后再启动。 + +**为什么**:这个服务虽然不接 Binance 私钥,但 DB 密码和代理凭据仍可能出现在 `.env`。同时 app 启动前若表不存在,会导致 collector/API 失败;端口变量混用会让健康检查和端口映射分叉。 + +**怎么验证**: +```bash +make guard + +# 部署机额外跑(本地无 docker 时可跳过,但发布前必须跑) +docker compose config +docker compose up -d +docker compose ps +curl -s http://localhost:${APP_HOST_PORT:-8080}/v1/health +``` + +**例外**:无。若改为 Kubernetes / Helm / systemd 部署,也必须保留同等约束:secret 不进镜像、migration 先于 app、readiness 可观测、容器内端口稳定。 + +--- + ## 升级守护规则的时机 同一类错误第二次出现 → 写一条新规则进来。修改本文件 = 改动了项目契约,需要在 PR 描述里说明原因并 @ 维护者。 diff --git a/ai/task-templates.md b/ai/task-templates.md index 456a92a..ef64375 100644 --- a/ai/task-templates.md +++ b/ai/task-templates.md @@ -10,7 +10,7 @@ objective: 一句话说要做的事 scope: 会改的文件 / 目录(具体路径,不要"相关文件") out-of-scope: 显式不改的范围(防止 scope creep) -constraints: 引用 ai/risk-guardrails.md 的哪几条(G1-G10) +constraints: 引用 ai/risk-guardrails.md 的哪几条(G1-G13) validation: 要跑哪些命令证明通过(必须可机械执行) acceptance: 完成判据(行为可观测的描述) ``` diff --git a/docker-compose.yml b/docker-compose.yml index f4d10d2..0fc1fef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: - ./migrations:/migrations:ro command: - -path=/migrations - - -database=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-hermes_market}?sslmode=disable + - -database=postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set, see .env.example}@postgres:5432/${POSTGRES_DB:-hermes_market}?sslmode=disable - up restart: "no" @@ -51,12 +51,12 @@ services: migrate: condition: service_completed_successfully ports: - - "${APP_PORT:-8080}:8080" + - "${APP_HOST_PORT:-8080}:8080" environment: CONFIG_PATH: /app/config/config.yml APP_ENV: ${APP_ENV:-production} - APP_PORT: ${APP_PORT:-8080} - POSTGRES_DSN: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-hermes_market}?sslmode=disable + APP_PORT: "8080" + POSTGRES_DSN: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set, see .env.example}@postgres:5432/${POSTGRES_DB:-hermes_market}?sslmode=disable BINANCE_BASE_URL: ${BINANCE_BASE_URL:-https://fapi.binance.com} # 透传代理:国内直连 fapi.binance.com 不稳,详见 AGENTS.md §7.5 HTTP_PROXY: ${HTTP_PROXY:-}