Make deployments pull immutable CI-built images while keeping test failures diagnosable before any package is published.
9.2 KiB
OSG Account Server 部署指南
本方案使用一个非 root、只读文件系统兼容的 JDK 21 容器。OpenResty 终止 TLS 并反向代理,
MySQL 复用已有实例。compose.yaml 不会创建数据库容器。
1. 前置条件
account.osglab.com与osglab.com已解析到 1Panel 主机。- Docker Engine、Docker Compose v2 和 1Panel OpenResty 已安装。
- 已有 MySQL 8 实例可从应用容器通过 Docker 外部网络或私网访问。
- 服务器时钟同步,Apple、DeepSeek、Volcengine 的出站 HTTPS/WSS 可用。
- App 的 Associated Domains 包含
applinks:osglab.com。
邀请使用一方 Universal Link,不依赖 Firebase Dynamic Links 或 Branch。
2. 注册邀请网页路由
网页实现在 features/inviteweb,资源在 src/main/resources/invite/index.html。组合根应只注册
一个 /i/{code} 路由;不要与旧 features/invite 路由同时挂载。ReferralLookupPort 是只读、
大小写敏感的验证边界,适配器应在 referrals 模块中用有界查询检查邀请码及活动状态:
先在 Koin 注册一个 ReferralLookupPort 实现;公共接线会从现有 AppConfig 读取 App Store URL、
邀请域名和 Apple Team ID/bundle ID:
routing {
configureInviteWebRoutes()
}
测试或不使用 Koin 的宿主可调用显式重载
configureInviteWebRoutes(referralLookup, InviteWebConfig(...))。
邀请码规则与当前生成器一致:16 字节随机值编码为无填充 Base64URL,即固定 22 位、区分大小写,
字符白名单为 A-Z、a-z、0-9、_、-。结构非法或查无记录均返回相同 404;查询超时或
失败返回不泄露内部信息的 503。页面和两个 AASA endpoint 均发送 no-store。
3. 准备现有 MySQL
先创建数据库,字符集使用 utf8mb4。使用 docs/mysql-minimum-privileges.sql 创建相互独立的
迁移账号和运行账号,并将示例来源网段替换为实际应用容器网段。不要使用公网来源或 %:
CREATE DATABASE osg_account
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
应用启动时使用迁移账号执行 Flyway,随后 Hikari 连接池只使用运行账号。运行账号不能更新或删除
不可变 ledger/usage 历史,也不能访问 Flyway 元数据或未来未审查的表。禁止授予全局权限、
FILE、PROCESS、SUPER、CREATE USER、GRANT OPTION。不要复用 root 或其他应用用户。
若 MySQL 是容器,把它和应用接入同一个已存在的外部网络:
docker network create account-backend
docker network connect account-backend <existing-mysql-container>
已存在网络时不要重复创建。DATABASE_URL 中使用该 MySQL 容器在网络内可解析的名称。若 MySQL
位于私网主机,使用私有 DNS/IP,并在数据库防火墙中只允许应用主机或容器网段。
4. 配置环境与秘密
复制部署所需变量到项目根目录的未跟踪 .env,或使用 1Panel 的环境变量/秘密管理。不要把真实
值写入 YAML、镜像层或 Git。
必须设置:
DATABASE_URL=jdbc:mysql://<existing-mysql>:3306/osg_account?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&forceConnectionTimeZoneToSession=trueDATABASE_USER、DATABASE_PASSWORDDATABASE_MIGRATION_USER、DATABASE_MIGRATION_PASSWORD:必须与运行账号及其密码不同JWT_SECRET:至少 32 个随机字节FIELD_ENCRYPTION_KEY:恰好 32 个随机字节的 Base64IDENTITY_HMAC_KEY:至少 32 个随机字节的独立 Base64,不得复用字段加密密钥APPLE_TEAM_ID、APPLE_KEY_ID、APPLE_PRIVATE_KEY_PEMVOLCENGINE_API_KEY、DEEPSEEK_API_KEYAPP_STORE_URL:正式 App Store HTTPS 地址;生产 Compose 不接受缺失值
生成新秘密的示例:
openssl rand -base64 48
openssl rand -base64 32
openssl rand -base64 32
三个结果分别用于 JWT、字段加密和身份 HMAC,不能相互复用。APPLE_PRIVATE_KEY_PEM 可使用包含
字面量 \n 的单行值;应用配置会在内存中还原换行。限制 .env 权限:
chmod 600 .env
生产 Compose 已固定 APP_ENV=production、Apple production 环境及两项完整性强制开关。
Apple 配置使用所属开发者账号的 Team ID、Key ID、bundle ID 和 .p8 私钥;火山引擎使用 SAUC
v3 WSS endpoint、资源 ID 和 API Key;DeepSeek 使用 HTTPS endpoint、已开通模型名和 API Key。
三方凭据分别创建、分别轮换,不得复用。
5. 构建与启动
GitHub CI 在测试通过后发布私有镜像
ghcr.io/hkgood/osg-account-server。先使用仅有 read:packages 权限的部署令牌登录 GHCR;
令牌不要写入 .env、Compose、1Panel 截图或 shell 历史。然后检查变量插值并拉取镜像。
docker compose config 会展开秘密,不要把输出上传或粘贴到工单:
./gradlew test
echo "$GHCR_TOKEN" | docker login ghcr.io -u hkgood --password-stdin
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
curl --fail http://127.0.0.1:18080/health/ready
容器以 UID/GID 10001 运行,根文件系统只读,仅 /tmp 是带大小限制的 tmpfs。应用端口只映射到
宿主机回环地址,不能改成 0.0.0.0。
查看日志时不得记录或复制 token、Apple subject、音频、prompt、转录或模型响应:
docker compose logs --since=10m account-server
6. 配置 1Panel / OpenResty
在 1Panel 创建 account.osglab.com、osglab.com 两个 HTTPS 网站并签发证书。使用
deploy/openresty-account.conf:
- 将
map、limit_req_zone、limit_req_status、upstream放入 OpenRestyhttp上下文。 - 将三个
server块作为站点配置;按 1Panel 实际证书路径调整ssl_certificate。 - 示例 upstream 指向宿主机
127.0.0.1:18080。若 OpenResty 自身在容器中,则将其加入account-backend,并改为account-server:8080。 - 配置明确对
/admin、/internal、/v1/admin返回 404;不要新增绕过该规则的泛域名代理。 - API 示例按 IP 限制 20 请求/秒,邀请页限制 5 请求/秒,可基于真实流量谨慎调整。
- 代理统一支持 HTTP/1.1 Upgrade/Connection,因此当前 HTTP API 与后续 WebSocket 入口都可用。
两个 AASA 地址由 Ktor 根据 appleAppId 模板输出,不需要复制静态文件。配置检查成功后再通过
1Panel 重载 OpenResty:
openresty -t
7. 验证
检查健康状态、反向代理、隐藏路径、限流和 AASA:
curl --fail https://account.osglab.com/health/ready
curl -i https://account.osglab.com/internal/
curl -i https://osglab.com/.well-known/apple-app-site-association
curl -i https://osglab.com/i/AbCdEf0123456789_-AbCd
预期结果:
- 健康检查返回 200。
- 内部路径返回 404。
- AASA 返回 200、
Content-Type: application/json,且不发生重定向。 - 合法 22 位邀请码返回双语 HTML;非法、未知或失效邀请码统一返回 404。
- 邀请页包含 nonce CSP、
Cache-Control: no-store、X-Robots-Tag,无第三方请求。
使用真机验证 Universal Link。安装带 Associated Domains entitlement 的 App 后,从信息或邮件点击
https://osglab.com/i/<code> 应直接进入 App;未安装时应显示网页。AASA 更新受系统缓存影响,
首次验证应预留传播时间。
8. 防火墙
- 公网入站仅允许 TCP 80/443。
- SSH 仅允许管理员固定 IP 或 VPN;不公开 18080、8080、3306。
- MySQL 3306 仅允许 Docker 私网或指定应用主机。
- 出站允许 DNS、NTP、Apple HTTPS、DeepSeek HTTPS、Volcengine HTTPS/WSS。
- 云安全组、主机防火墙和 1Panel 防火墙应采用相同边界,避免其中一层意外放行。
9. 更新与回滚
更新前备份 MySQL 并记录当前镜像标签。使用不可变标签构建:
IMAGE_TAG=sha-<commit> docker compose pull
IMAGE_TAG=sha-<commit> docker compose up -d
Flyway 迁移只向前执行。若新版本包含数据库迁移,应用镜像回滚不等于数据库回滚;应先按迁移影响
制定恢复方案。无数据库变更时,可把 IMAGE_TAG 切回上一版本并重新执行 docker compose up -d。
备份、时间点恢复和每月恢复演练按 docs/BACKUP.md 执行。
10. 分阶段验收
- 本地阶段:
./gradlew test通过;合法、未知、格式错误和 lookup 异常路径符合预期;HTML 不加载第三方资源。 - 容器阶段:镜像以 UID/GID 10001 运行;根文件系统只读;
/health/live与/health/ready通过;容器无法取得额外 Linux capability。 - 预发布阶段:Flyway 使用迁移账号成功,应用使用运行账号成功;撤销运行账号 DDL 权限后服务 仍正常;OpenResty HTTP、HTTPS 和 WSS 代理验证通过。
- Apple 阶段:两个 AASA 地址返回无重定向 JSON;真机从信息或邮件点击
https://osglab.com/i/<code>可打开 App;未安装 App 时显示双语落地页。 - 生产阶段:公网仅开放 80/443,3306/8080/18080 不可达;邀请 URL 不出现在访问日志; 供应商凭据可用;完成加密备份并记录一次隔离恢复结果。