Files
ballet-server/backend/DEPLOYMENT.md
T
yuxuanhui 5d2fbfaa1b
Deploy backend / deploy (push) Successful in 40s
feat: 统一后端日志并记录接口请求
2026-09-30 11:52:13 +08:00

8.6 KiB
Raw Blame History

Gitea + Docker 生产部署

生产 Compose 只部署 Go API,连接已由其他服务管理的 PostgreSQL;数据库可以在同一主机,也可以在另一台服务器。本地开发继续使用包含 PostgreSQL 的 compose.yaml。小程序仍由微信开发者工具发布,构建时的 TARO_APP_API_BASE_URL 应指向此 API 的 HTTPS 域名。

首次准备

  1. 在目标 Linux 主机安装 Docker Engine、Docker Compose、Git、Bash、flock 和 curl。按照 Gitea Runner 文档在这台主机上注册仓库专用的 act_runner,为它配置 tencent-prod:host 标签,并确保 Runner 用户可以执行 Docker 命令和 actions/checkout@v4。此工作流在宿主机执行代码且拥有 Docker 权限,只应给可信仓库和维护者使用。
  2. 在 Gitea 仓库启用 Actions,并按密钥文档设置 DATABASE_URL、WECHAT_APP_ID、WECHAT_APP_SECRET。数据库连接只使用一个 DATABASE_URL 密钥;WECHAT_APP_ID 必须与小程序 AppID 一致。缺少密钥会在 Compose 校验时失败;校验使用 config --quiet,不会打印展开后的密钥。
  3. DATABASE_URL 使用 postgresql://用户名:已编码密码@数据库主机:端口/数据库名?sslmode=模式,例如 postgresql://ballet_island:<encoded-password>@db.example.com:5432/ballet_island?sslmode=verify-full。数据库主机必须从 API 容器内部可解析、可访问,不能把容器内的 127.0.0.1 当作另一台主机。密码中的 @、:、/、?、#、% 等 URL 特殊字符须按 PostgreSQL 连接 URI 规则做百分号编码。sslmode 按数据库实际 TLS 配置填写:服务器未启用 TLS 时用 disable;有可验证证书时优先用 verify-full,使用私有 CA 时还需让 API 容器信任该 CA。跨服务器还需放通应用主机到数据库的连接,并确认数据库允许该用户和来源地址访问。
  4. 确保宿主机 127.0.0.1:8113 空闲。HTTPS 反向代理将 API 域名转发到该地址,并在微信小程序后台配置 request 合法域名。生产 Compose 仅将 API 绑定到回环地址,不发布数据库端口。
  5. 首次上线前在统一数据库服务器上创建数据库和账号,确定备份位置及恢复步骤。本地 compose.yaml 的 ballet-island_postgres_data 卷不会被生产 Compose 使用;若数据仍在该卷中,须先迁移到统一数据库服务器。

发布流程

推送 main 中的 backend/** 或工作流变更会触发部署,也可在 Gitea Actions 中手动运行。Runner 从 Gitea 检出代码,在目标主机取得部署锁,并确认检出的提交仍是远端 main 最新提交。然后校验生产 Compose、以提交短 SHA 构建 API 镜像、更新 ballet-island Compose 项目,等待 API 的数据库就绪检查通过,再访问宿主机 /readyz。工作流结束时输出该项目的容器状态。

生产配置通过 --env-file /dev/null 忽略开发 .env,数据库连接完全由 Gitea 的 DATABASE_URL 指定。后端在未设置 DATABASE_URL 时仍使用本地开发的 PG* 环境变量。生产 Compose 不创建 PostgreSQL 容器,也不要求与数据库容器共享 Docker 网络;如果数据库仅通过另一 Compose 项目的内部服务名开放,须先提供 API 容器可达的网络和地址。工作流未使用 --remove-orphans,切换前若已有同名项目中的旧 PostgreSQL 容器,不会自动停止或清理它。

API 启动时会在事务和数据库锁保护下应用尚未执行的版本迁移,当前没有独立迁移命令。发布前应在统一数据库服务器上备份,未来涉及不兼容 schema 的改动须先制定迁移与回退顺序。工作流不会自动回滚数据库;失败后先检查容器状态和日志,再决定恢复备份或重新部署。

集中日志(Alloy → Loki)

生产 API 已在服务级配置共享日志平台使用的 Docker 标签:observability.logs=true、observability.project=ballet-island、observability.service=api、observability.env=production。后端使用 slog 将 JSON 日志输出到 stdout;Docker 使用 json-file 驱动,按 max-size: "20m"、max-file: "5" 轮转。这是容器本地日志轮转配置,Loki 的保留时间由共享平台单独管理。Docker 日志轮转说明

日志由 internal/logging 统一配置,入口使用 slog.SetDefault(logging.New(os.Stdout))。非请求日志继续使用 slog;处理请求时通过 logging.FromContext(r.Context()) 获取带请求编号的 logger,业务代码只传入可公开的结构化字段,不记录原始数据库或网络错误。请求中间件在最外层路由接入一次,避免重复记录。

每次请求完成输出一条 http request completed,包含 request_id、method、route、status 和 duration_ms;2xx/3xx 使用 INFO、4xx 使用 WARN、5xx 使用 ERROR。健康检查也会记录。route 是匹配的路由模板(例如 GET /v1/records/{id}),未匹配时为 unmatched;不记录实际路径参数、查询参数、请求头或请求体。request_id 由服务端生成并通过响应头 X-Request-ID 返回,客户端提供的同名头不会被采用;业务异常日志携带相同编号。请求因 panic 中断时输出 ERROR 级别的 http request aborted,保留已有响应状态,尚未发送响应则记为 0,仍由 Go HTTP 服务处理 panic。

接入前提是 API 所在主机已有 Alloy,能够通过该主机的 Docker API 读取容器日志,按 observability.logs=true 发现容器,将其余三个标签映射为 Loki 的 project、service、env,并发送到可达的 Loki。host 标签由 Alloy 按实际主机设置。该采集方式不要求 API 加入日志平台网络或在业务镜像内安装 Alloy。本仓库只配置业务容器,Alloy、Loki 和 Grafana 由共享平台管理。Alloy Docker 日志采集说明

沿用上述发布流程,docker compose up 会重新创建配置发生变化的 API 容器以应用标签和日志驱动;仅执行 restart 不会应用这些变更。部署后在应用主机检查实际容器:

docker ps \
  --filter label=com.docker.compose.project=ballet-island \
  --filter label=com.docker.compose.service=api \
  --format '{{.Names}} logs={{.Label "observability.logs"}} project={{.Label "observability.project"}} service={{.Label "observability.service"}} env={{.Label "observability.env"}}'

预期标签为 logs=true project=ballet-island service=api env=production。随后在 Grafana Explore 选择 Loki,时间范围选最近一小时,查询:

{project="ballet-island", service="api", env="production"} |= "server starting"

核对查询结果包含本次部署产生的新启动日志,时间和主机正确,才算日志链路验收通过。容器健康或 Alloy/Loki 就绪不能代替此项验收。本配置仅接入 API 日志,外部 PostgreSQL 日志、主机指标和告警由各自部署单独配置。

请求日志需要部署包含上述模块的新 API 镜像后生效。可在应用主机执行 curl -i http://127.0.0.1:8113/healthz,记下响应头 X-Request-ID,在 Grafana 中查询访问日志:

{project="ballet-island", service="api", env="production"} |= "http request completed"

再用 {project="ballet-island", service="api", env="production"} |= "<X-Request-ID 的值>" 查找同一次请求的全部日志,核对状态、耗时与请求编号。请求编号保留为 JSON 字段,无需改动现有容器采集标签。

验收与回退

发布成功后,从服务器确认 curl -f http://127.0.0.1:8113/readyz,并从外部确认 HTTPS 域名的 /readyz。真实微信登录、合法域名及小程序构建产物仍需单独验收;/readyz 只证明 API 可连接数据库。

每个发布镜像保留 ballet-island-api:<提交短 SHA> 标签。要回到某个已构建版本,应先确认其镜像仍在目标主机上,并评估数据库迁移是否与旧程序兼容;随后用同一组生产环境变量设置 IMAGE_TAG=<旧提交短 SHA>,在 backend/ 执行:

docker compose --env-file /dev/null -f compose.prod.yaml up -d --wait --wait-timeout 180 --no-build

回退镜像不会回退数据库迁移。修改 Gitea 中 DATABASE_URL 的密码也不会修改统一数据库服务器里的账号密码,两处需要按数据库运维流程同步更新。