deploy: harden compose harness

This commit is contained in:
dela
2026-05-24 21:18:19 +08:00
parent ea22d06dab
commit 542eeefdbd
9 changed files with 110 additions and 13 deletions

61
.dockerignore Normal file
View File

@@ -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/

View File

@@ -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 ----

View File

@@ -84,6 +84,9 @@ go test ./internal/<pkg>/...
# 全部测试
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` 固定 8080migration 必须在 app 前完成;`.dockerignore` 必须排除 `.env` / `.env.*`
---

View File

@@ -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:

View File

@@ -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 中的 DSNDocker Compose 里就是这么干的) |
| `APP_PORT` | HTTP 端口 |
| `APP_PORT` | HTTP 端口compose 部署中容器内固定为 8080 |
| `APP_HOST_PORT` | docker compose 暴露到宿主机的 HTTP 端口 |
| `HTTPS_PROXY` | 走代理访问 Binance |
---

View File

@@ -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 ← 本文件

View File

@@ -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 描述里说明原因并 @ 维护者。

View File

@@ -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: 完成判据(行为可观测的描述)
```

View File

@@ -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:-}