526 lines
32 KiB
Markdown
526 lines
32 KiB
Markdown
# CreatorHub 部署
|
||
|
||
本文档按当前发布提交说明单台 Linux 主机 Docker Compose 部署及离线检查,不证明新产品功能已实现。[plan01](plan01.md) 是业务范围与验收依据;离线结果不能替代抖音/小红书最终真机验收。
|
||
|
||
当前控制面仍使用单用户 HTTP Basic Auth(除 `/healthz` 外,包括静态页面),不提供 RBAC 或多租户隔离。开发目标不新增认证/访问限制,但本轮未删除现有代码或配置;以下变量仍须填写,不新增认证 profile。
|
||
|
||
## 部署内容
|
||
|
||
`compose.yaml` 会启动以下服务:
|
||
|
||
- `creator-hub`:Go 1.26 控制面,同时提供 React 19/Vite 8 构建的静态页面;
|
||
- `docker-gateway`:受限 Docker API 网关,是唯一挂载 `/var/run/docker.sock` 的服务;
|
||
- `postgres`:PostgreSQL 17,数据保存在 `creatorhub_postgres` 命名卷;
|
||
- 浏览器容器:由网关按需创建,镜像引用由平台「镜像版本」页配置(缺失时网关自动拉取),Profile 保存在 `creatorhub-profile-<别名>` 命名卷。
|
||
|
||
控制面发布到宿主机所有网卡,局域网内可直接访问;浏览器容器可访问外网。
|
||
`creator-hub` 会等待 `docker-gateway` 健康检查通过后再启动。
|
||
|
||
## 前置条件
|
||
|
||
- Linux 主机;
|
||
- Docker Engine 26 或兼容版本;
|
||
- Docker Compose v2;
|
||
- 当前用户可访问 Docker daemon;
|
||
- 可访问镜像仓库(如 `git.ipao.vip`),并已完成登录(如仓库要求认证);镜像也可不在宿主机预拉取,网关会在缺失时按引用自动拉取;
|
||
- `curl`、`jq`、`openssl` 和 GNU `stat`,用于启动与业务验证命令;
|
||
- 若验证真实浏览器运行环境:Linux x86_64、一个可访问的固定 HTTP/HTTPS/SOCKS 出口,以及可被 Docker daemon 拉取的、以 immutable digest 固定的 `fingerprint-chromium` 镜像。镜像来源必须登记仓库地址、构建提交和 digest,禁止使用 `latest` 或未登记 tag;Xvfb/CDP 包装入口的可复现源码见 [`docker/browser-wrapper`](../docker/browser-wrapper/)。
|
||
|
||
在仓库根目录执行预检:
|
||
|
||
```bash
|
||
test -S /var/run/docker.sock
|
||
docker info >/dev/null
|
||
docker compose version
|
||
: "${DOCKER_GID:?export DOCKER_GID=$(stat -c '%g' /var/run/docker.sock) in this shell}"
|
||
: "${CONTROL_PLANE_USERNAME:?export CONTROL_PLANE_USERNAME in this shell}"
|
||
: "${CONTROL_PLANE_PASSWORD:?export CONTROL_PLANE_PASSWORD in this shell}"
|
||
: "${CREATORHUB_CREDENTIAL_MASTER_KEY:?export CREATORHUB_CREDENTIAL_MASTER_KEY in this shell}"
|
||
docker compose config --quiet
|
||
```
|
||
|
||
## 首次部署
|
||
|
||
以下命令应在同一个 shell、仓库根目录执行。本说明采用显式导出模式:Compose 启动和手工验证必须读取当前 shell 中导出的同一组变量;验证脚本不会自行读取 `.env`,也不会使用 Compose 默认值。若变量只写在 `.env` 中,请先将相同值 export 到当前 shell。`DOCKER_GID` 必须与宿主机 Docker socket 的组一致;`CREATORHUB_PORT` 控制控制面的宿主机端口。
|
||
|
||
```bash
|
||
export DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)"
|
||
export CREATORHUB_PORT=8080
|
||
export GATEWAY_TOKEN="$(openssl rand -hex 24)"
|
||
export CONTROL_PLANE_USERNAME=creatorhub
|
||
export CONTROL_PLANE_PASSWORD="$(openssl rand -hex 24)"
|
||
export CREATORHUB_CREDENTIAL_MASTER_KEY="$(openssl rand -base64 32)"
|
||
|
||
docker compose config --quiet
|
||
docker compose up --detach --build
|
||
```
|
||
|
||
网关配置了 45 秒停止宽限期,并在收到 SIGTERM/SIGINT 时停止接收请求、排空有限期限内的在途请求,再关闭事件订阅和代理;本地测试覆盖该顺序,但部署验证仍须记录正常退出码、实际耗时和没有 SIGKILL,不能仅以“等待了 45 秒”证明优雅退出。
|
||
|
||
控制面启动时会连接 PostgreSQL,并在事务和 advisory lock 保护下自动执行前向迁移。迁移失败时控制面会退出,由 Compose 按 `restart: unless-stopped` 重启;先检查日志,不要删除数据卷。
|
||
|
||
### 首次配置
|
||
|
||
服务起来后打开 <http://127.0.0.1:${CREATORHUB_PORT}>:
|
||
|
||
1. 「网关管理」页注册网关:名称如 `gw-main`,Endpoint `http://docker-gateway:8081`,令牌填 `GATEWAY_TOKEN` 的值(即 `openssl rand -hex 24` 生成的值)。
|
||
2. 按 [`docker/browser-wrapper/README.md`](../docker/browser-wrapper/README.md) 用已登记 base digest 构建包装镜像,再在「镜像版本」页添加发布 digest。生产与验收必须填写 `git.ipao.vip/rogee/creatorhub-browser-wrapper@sha256:<已登记摘要>`;`148.0.7778.215` 只作为浏览器版本元数据,不能单独作为不可变引用。登记内容同时包含 base 镜像源仓库、base digest、构建提交、Dockerfile 路径和发布摘要。
|
||
3. 「社媒账号」页先创建账号(默认暂停),再在「运行环境」选该账号、网关、镜像,填写中文名、小写别名及指纹参数。代理可选;指定代理须先手动检测为健康,留空是明确直连,不是失败回退。
|
||
4. 创建得到停止态容器;在账号页恢复账号后回环境页显式启动。容器名 `creatorhub-browser-<别名>`,Profile 卷 `creatorhub-profile-<别名>`。回收只删除容器、保留环境/binding/Profile;完整契约见[架构说明](architecture/container-control.md)。当前直连 create/start 不代表 upgrade/rebind 已支持空出口。
|
||
|
||
### 人工登录二维码
|
||
|
||
抖音账号页的「显示登录二维码」由 CreatorHub 通过已绑定 gateway 请求登录画面,并直接展示在后台;它不会注入密码、Cookie 或自动完成登录。登录画面只在内存中返回,前端显示两分钟有效期;完成扫码或验证码后,点击「核验浏览器身份」确认 UID 与账号绑定一致。gateway 无法取得二维码时,后台展示实际登录/验证码画面并要求人工处理,不把不确定结果标记为成功。
|
||
|
||
对应接口为 `POST /api/creator/accounts/<account_id>/login-qr`,仅接受已授权、已运行的抖音账号;响应中的 `image_base64` 只用于当前页面展示,不应写入日志、数据库或备份。
|
||
|
||
## 部署验证
|
||
|
||
```bash
|
||
curl --fail --silent --show-error \
|
||
--retry 30 --retry-delay 2 --retry-connrefused \
|
||
--output /dev/null "http://127.0.0.1:${CREATORHUB_PORT}/healthz"
|
||
|
||
curl --fail --silent --show-error \
|
||
--retry 30 --retry-delay 2 --retry-connrefused \
|
||
--output /dev/null "http://127.0.0.1:${CREATORHUB_PORT}/readyz"
|
||
|
||
curl --fail --silent --show-error \
|
||
--retry 30 --retry-delay 2 --retry-connrefused \
|
||
--user "${CONTROL_PLANE_USERNAME}:${CONTROL_PLANE_PASSWORD}" \
|
||
"http://127.0.0.1:${CREATORHUB_PORT}/api/browsers" >/dev/null
|
||
|
||
docker compose exec -T postgres \
|
||
psql -U creatorhub -d creatorhub -tAc \
|
||
'SELECT 1 FROM schema_migration WHERE version = 32;' \
|
||
| grep -qx 1
|
||
docker compose exec -T postgres \
|
||
psql -U creatorhub -d creatorhub -tAc \
|
||
'SELECT 1 FROM schema_migration WHERE version = 33;' \
|
||
| grep -qx 1
|
||
|
||
docker compose ps
|
||
```
|
||
|
||
健康检查应成功,浏览器列表接口应返回 JSON,迁移查询当前应输出 `1`,三个 Compose 服务应为运行状态。`CREATORHUB_CREDENTIAL_MASTER_KEY` 必须由部署侧 Secret Manager/OS Keyring 持久保存并在每次启动时注入同一值;账号凭据以 AES-GCM 密文写入独立 `creatorhub_credentials` 卷,轮换主密钥前必须先迁移已有凭据。然后访问 <http://127.0.0.1:8080>;修改过 `CREATORHUB_PORT` 时使用对应端口。
|
||
|
||
旧实验版本的数据库异常应先记录版本、备份并核实,不把历史文档中的建议迁移当成本期开发要求;本期不新增兼容迁移、回填或双写。下文更新/恢复命令仅描述现有部署的数据操作,不改变 plan01 验收范围。
|
||
|
||
排障时读取结构化服务日志:
|
||
|
||
```bash
|
||
docker compose logs --tail=200 creator-hub docker-gateway postgres
|
||
```
|
||
|
||
## 手工业务验证(阶段 A Mock)
|
||
|
||
本节的旧阶段 A 业务执行器是 Mock,不向真实社交平台发送;容器、数据库、网关和可选代理则是实际运行资源。最小业务路径是:账号 → 草稿 → 显式确认 → 任务入队 → Mock 执行 → 审计回溯。
|
||
|
||
页面路径如下:
|
||
|
||
1. 登录后在「网关管理」注册 <http://docker-gateway:8081>,令牌必须等于 GATEWAY_TOKEN。
|
||
2. 在「镜像版本」添加并启用一个可拉取的 fingerprint-chromium 镜像。
|
||
3. 可选:在「网络出口」创建出口并点击「检测」,健康状态必须为「健康」;这里只填写凭据引用 ID,不填写密码、Cookie 或 token。留空则使用网关所在机器的网络出口直连。
|
||
4. 在「社媒账号」创建平台为抖音的账号;创建后默认暂停,后续仍可使用 Mock 执行器验证离线闭环。
|
||
5. 在「运行环境」选择该账号、镜像和可选的健康出口,使用正整数 Fingerprint Seed 创建环境;随后在账号详情点击「恢复账号」,再在「运行环境」点击「启动」。
|
||
6. 在账号详情创建文本草稿,点击「核对草稿」,勾选“我已核对当前账号、草稿内容、运行环境和固定出口”,依次执行「确认当前快照」→「保存确认」→「加入队列」。
|
||
7. Mock 执行器没有独立页面,使用下面的 POST /api/phase-a/mock/execute,再到「任务中心」和「审计」核对结果。
|
||
|
||
以下命令可在已经启动的仓库根目录直接执行,并且必须在启动 Compose 的同一个 shell 中执行。开始前确认 `CREATORHUB_PORT`、`GATEWAY_TOKEN`、`CONTROL_PLANE_USERNAME` 和 `CONTROL_PLANE_PASSWORD` 都已 export;脚本不会读取未 export 的 `.env` 值或 Compose 默认值。出口参数必须替换成已批准且可从 creator-hub 容器访问的无认证代理;若代理需要认证,先按部署规范准备 credential_reference,不要把秘密值写进命令或仓库。镜像引用也可改成已发布的 registry 引用;本地 smoke test 可以使用宿主机上已有的镜像标签。
|
||
|
||
~~~bash
|
||
set -Eeuo pipefail
|
||
|
||
: "${CREATORHUB_PORT:?export CREATORHUB_PORT (same value used by Compose)}"
|
||
: "${CONTROL_PLANE_USERNAME:?set CONTROL_PLANE_USERNAME}"
|
||
: "${CONTROL_PLANE_PASSWORD:?set CONTROL_PLANE_PASSWORD}"
|
||
: "${GATEWAY_TOKEN:?export GATEWAY_TOKEN (same value registered in the gateway)}"
|
||
: "${PROXY_HOST:?set PROXY_HOST to an approved fixed proxy host}"
|
||
: "${PROXY_PORT:?set PROXY_PORT to an approved fixed proxy port}"
|
||
BASE_URL="http://127.0.0.1:${CREATORHUB_PORT}"
|
||
|
||
api() {
|
||
curl --fail-with-body --silent --show-error \
|
||
--user "${CONTROL_PLANE_USERNAME}:${CONTROL_PLANE_PASSWORD}" \
|
||
-H 'Content-Type: application/json' "$@"
|
||
}
|
||
|
||
[[ "$PROXY_PORT" =~ ^[0-9]+$ ]] && (( PROXY_PORT >= 1 && PROXY_PORT <= 65535 ))
|
||
|
||
run_id="manual-$(date +%s)"
|
||
gateway_name="gw-${run_id}"
|
||
image_version="${IMAGE_VERSION:-148.0.7778.215}"
|
||
: "${IMAGE_REF:?IMAGE_REF must be an immutable browser-wrapper @sha256 reference}"
|
||
case "$IMAGE_REF" in
|
||
*@sha256:*) image_ref="$IMAGE_REF" ;;
|
||
*) echo "IMAGE_REF must contain @sha256:" >&2; exit 2 ;;
|
||
esac
|
||
exit_protocol="${PROXY_PROTOCOL:-http}"
|
||
account_key="${run_id}-platform"
|
||
env_alias="${run_id}-env"
|
||
|
||
gateway_json="$(api -X POST "$BASE_URL/api/gateways" --data "$(jq -n \
|
||
--arg name "$gateway_name" --arg token "$GATEWAY_TOKEN" \
|
||
'{name:$name,endpoint:"http://docker-gateway:8081",token:$token}')")"
|
||
jq -e --arg name "$gateway_name" '.name == $name' <<<"$gateway_json" >/dev/null
|
||
|
||
image_json="$(api -X POST "$BASE_URL/api/browser-images" --data "$(jq -n \
|
||
--arg version "$image_version" --arg ref "$image_ref" \
|
||
'{version:$version,image_ref:$ref,note:"manual validation",enabled:true}')")"
|
||
jq -e --arg version "$image_version" '.version == $version and .enabled == true' <<<"$image_json" >/dev/null
|
||
|
||
exit_json="$(api -X POST "$BASE_URL/api/network-exits" --data "$(jq -n \
|
||
--arg protocol "$exit_protocol" --arg host "$PROXY_HOST" --argjson port "$PROXY_PORT" \
|
||
'{protocol:$protocol,host:$host,port:$port}')")"
|
||
exit_id="$(jq -er '.id' <<<"$exit_json")"
|
||
checked_exit="$(api -X POST "$BASE_URL/api/network-exits/$exit_id/check")"
|
||
jq -e '.health_status == "healthy"' <<<"$checked_exit" >/dev/null
|
||
|
||
account_json="$(api -X POST "$BASE_URL/api/phase-a/accounts" --data "$(jq -n \
|
||
--arg name "手工验证账号" --arg key "$account_key" --arg cookies "${ACCOUNT_COOKIES:-sessionid=manual-test}" \
|
||
'{name:$name,platform:"douyin",platform_account_key:$key,tags:["manual"],cookies:$cookies}')")"
|
||
account_id="$(jq -er '.id' <<<"$account_json")"
|
||
jq -e '.authorization_status == "authorized" and .runtime_status == "paused"' <<<"$account_json" >/dev/null
|
||
|
||
created_env="$(api -X POST "$BASE_URL/api/browsers" --data "$(jq -n \
|
||
--arg alias "$env_alias" --arg name "手工验证环境" --arg gateway "$gateway_name" \
|
||
--arg version "$image_version" --arg account "$account_id" --arg exit "$exit_id" \
|
||
'{alias:$alias,name:$name,gateway:$gateway,image_version:$version,account_id:$account,network_exit_id:$exit,fingerprint:{seed:1000}}')")"
|
||
jq -e --arg alias "$env_alias" '.alias == $alias' <<<"$created_env" >/dev/null
|
||
|
||
api -X POST "$BASE_URL/api/phase-a/accounts/$account_id/resume" >/dev/null
|
||
api -X POST "$BASE_URL/api/browsers/$env_alias/start" >/dev/null
|
||
|
||
for _ in $(seq 1 30); do
|
||
runtime_json="$(api "$BASE_URL/api/browsers/$env_alias")"
|
||
if jq -e '.runtime_id != "" and .runtime_instance_id != ""' <<<"$runtime_json" >/dev/null; then
|
||
break
|
||
fi
|
||
sleep 1
|
||
done
|
||
jq -e '.runtime_id != "" and .runtime_instance_id != ""' <<<"$runtime_json" >/dev/null
|
||
|
||
account_json="$(api "$BASE_URL/api/phase-a/accounts/$account_id")"
|
||
account_version="$(jq -er '.version' <<<"$account_json")"
|
||
draft_json="$(api -X POST "$BASE_URL/api/phase-a/drafts" --data "$(jq -n \
|
||
--arg account "$account_id" '{account_id:$account,content:"阶段 A 手工验证内容"}')")"
|
||
draft_id="$(jq -er '.id' <<<"$draft_json")"
|
||
draft_version="$(jq -er '.version' <<<"$draft_json")"
|
||
|
||
confirmation_json="$(api -X POST "$BASE_URL/api/phase-a/confirmations" --data "$(jq -n \
|
||
--arg draft "$draft_id" --argjson account_version "$account_version" --argjson draft_version "$draft_version" \
|
||
'{draft_id:$draft,account_version:$account_version,draft_version:$draft_version}')")"
|
||
confirmation_id="$(jq -er '.id' <<<"$confirmation_json")"
|
||
|
||
task_json="$(api -X POST "$BASE_URL/api/phase-a/tasks" --data "$(jq -n --arg confirmation "$confirmation_id" '{confirmation_id:$confirmation}')")"
|
||
task_id="$(jq -er '.id' <<<"$task_json")"
|
||
jq -e '.state == "queued"' <<<"$task_json" >/dev/null
|
||
duplicate_task_json="$(api -X POST "$BASE_URL/api/phase-a/tasks" --data "$(jq -n --arg confirmation "$confirmation_id" '{confirmation_id:$confirmation}')")"
|
||
jq -e --arg task_id "$task_id" '.id == $task_id' <<<"$duplicate_task_json" >/dev/null
|
||
|
||
execution_json="$(api -X POST "$BASE_URL/api/phase-a/mock/execute" --data \
|
||
'{"worker_id":"manual-worker","outcome":"succeeded"}')"
|
||
jq -e '.was_claimed == true and .state == "succeeded"' <<<"$execution_json" >/dev/null
|
||
|
||
task_detail="$(api "$BASE_URL/api/phase-a/tasks/$task_id")"
|
||
jq -e '(.state == "succeeded") and ((.attempts | length) >= 1) and (.attempts[0].evidence.mock_outcome == "succeeded")' <<<"$task_detail" >/dev/null
|
||
|
||
audit_json="$(api "$BASE_URL/api/phase-a/audit?task_id=$task_id&page_size=100")"
|
||
for event_type in task_queued task_claimed task_finished; do
|
||
jq -e --arg event_type "$event_type" '.data | any(.[]; .event_type == $event_type)' <<<"$audit_json" >/dev/null
|
||
done
|
||
|
||
printf 'PASS account=%s environment=%s task=%s state=succeeded audit=task_queued,task_claimed,task_finished\n' \
|
||
"$account_id" "$env_alias" "$task_id"
|
||
~~~
|
||
|
||
通过标准:/healthz 返回 204;受保护的 /api/browsers 返回 JSON;网关、控制面、PostgreSQL 均为运行状态;运行环境有 runtime_id 和 runtime_instance_id;任务最终为 succeeded,且执行尝试的脱敏证据为 mock_outcome=succeeded;审计至少包含 task_queued、task_claimed、task_finished。重复提交同一确认时应返回同一任务 ID,不应产生第二条任务。
|
||
|
||
验证完成后的容器回收(保留账号、环境/binding、PostgreSQL 数据与 Profile 卷,不是永久删除环境):
|
||
|
||
~~~bash
|
||
api() {
|
||
curl --fail-with-body --silent --show-error \
|
||
--user "${CONTROL_PLANE_USERNAME}:${CONTROL_PLANE_PASSWORD}" \
|
||
-H 'Content-Type: application/json' "$@"
|
||
}
|
||
api -X POST "$BASE_URL/api/browsers/$env_alias/stop" >/dev/null
|
||
api -X DELETE "$BASE_URL/api/browsers/$env_alias" >/dev/null
|
||
docker compose stop
|
||
~~~
|
||
|
||
## 常见故障排查
|
||
|
||
| 现象 | 先检查 | 处理 |
|
||
| --- | --- | --- |
|
||
| docker compose config 报 required | CONTROL_PLANE_USERNAME、CONTROL_PLANE_PASSWORD、CREATORHUB_CREDENTIAL_MASTER_KEY 是否在当前 shell 非空 | 重新 export 三个变量;用户名不能含冒号,密码至少 6 字节,主密钥必须为 32 字节的 base64 |
|
||
| 手工验证脚本在 `:?` 处退出 | CREATORHUB_PORT、GATEWAY_TOKEN、CONTROL_PLANE_USERNAME、CONTROL_PLANE_PASSWORD、CREATORHUB_CREDENTIAL_MASTER_KEY 是否都已 export | 在启动 Compose 的同一个 shell 中 export 完整变量集;不要只依赖 `.env` 或 Compose 默认值 |
|
||
| creator-hub 未启动 | docker compose ps、docker compose logs --tail=200 postgres docker-gateway creator-hub | 先确认 PostgreSQL 与网关 health 为 healthy;网关需能访问 /var/run/docker.sock,DOCKER_GID 使用 stat -c '%g' /var/run/docker.sock 的实际值 |
|
||
| API 返回 401 | curl 是否带 --user CONTROL_PLANE_USERNAME:CONTROL_PLANE_PASSWORD | /healthz 不需要认证,其余 /api/* 需要控制面 Basic Auth |
|
||
| 生命周期动作报网关不可用 | 网关注册的 Endpoint、令牌与 Compose 的 GATEWAY_TOKEN | Endpoint 应为 <http://docker-gateway:8081>;令牌必须完全一致。列表/详情只读持久记录,其成功不能证明网关在线 |
|
||
| 出口一直是 unchecked/unhealthy | 出口协议、主机、端口;控制面容器到代理的连通性;last_check_reason | 先用无认证代理完成最小验证;有认证时只提供已配置的凭据引用,不把认证值放到请求、日志或文档 |
|
||
| 创建环境时报 image_unavailable 或拉取超时 | image_ref 格式、镜像架构、Docker daemon 的 registry 登录和网络 | 版本表中的镜像必须可被 Docker daemon 拉取,最长约 10 分钟;已保存环境/binding 不会因网关失败自动删掉,核对记录后按原配置恢复 |
|
||
| 恢复/入队返回 503 | readiness、GET /api/browsers/<alias>、GET /api/network-exits/<id> | binding_missing、network_exit_unhealthy、runtime_missing 表示固定资源未就绪;先修复出口并启动原环境,不要换出口重试 |
|
||
| Mock 执行没有领取任务 | GET /api/phase-a/tasks/<id> 的 state、hold_reason | POST /api/phase-a/mock/execute 只领取满足账号、确认、已绑定出口健康(显式直连无此项)、活动 runtime 和租约条件的 queued 任务;policy_hold/needs_confirmation 不会自动重试 |
|
||
| 停止 Compose 后浏览器仍在运行 | docker ps --filter 'name=^creatorhub-browser-' | 动态浏览器不由 Compose 管理;先通过「运行环境」停止/回收,再 docker compose stop,不要误删 Profile 卷 |
|
||
|
||
不要执行 docker compose down --volumes 作为普通排障手段;它会删除 PostgreSQL 数据卷。不要把控制面密码、网关令牌、代理密码、Cookie 或 token 写入仓库、截图、日志或审计查询。
|
||
|
||
## 配置
|
||
|
||
Compose 部署时通常只需设置以下宿主机变量:
|
||
|
||
| 变量 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `CREATORHUB_PORT` | `8080` | 控制面宿主机端口,局域网可访问 |
|
||
| `DOCKER_GID` | 无(必填) | Docker socket 的宿主机组 ID;必须按实际值设置 |
|
||
| `GATEWAY_TOKEN` | `dev-creatorhub-gateway-token` | Compose 未提供变量时的默认值;本说明要求显式 export 随机值,并同步填入网关注册表单 |
|
||
| `CONTROL_PLANE_USERNAME` | 无(必填) | 控制面唯一用户;不能包含冒号 |
|
||
| `CONTROL_PLANE_PASSWORD` | 无(必填) | 控制面密码,至少 6 字节;使用随机值 |
|
||
| `CREATORHUB_CREDENTIAL_MASTER_KEY` | 无(必填) | 32 字节 base64;由部署 Secret Manager/OS Keyring 持久注入,重启后必须保持一致 |
|
||
| `BAILIAN_API_KEY` | 空(按需) | 已批准的生产 AI API key;为空时文本 AI 动作明确返回 unavailable,不使用 Mock |
|
||
| `BAILIAN_BASE_URL` | 客户端默认值 | 无用户信息的 HTTP(S) 地址;供应商变更前需完成审批和脱敏样本验证 |
|
||
| `CREATOR_MEDIA_DIR` | `/var/lib/creatorhub/materials` | 持久媒体目录;必须挂载持久卷,不能使用临时目录替代 |
|
||
| `CREATOR_TRANSCRIPTION_BIN` | 空(按需) | 可执行的本地转写入口;为空时转写明确失败,不伪造 succeeded |
|
||
|
||
服务本身支持并校验以下环境变量;`compose.yaml` 会在控制面凭据缺失或为空时拒绝渲染。下表中的 Compose 默认值不由手工验证脚本隐式读取;手工验证沿用上文的显式 export 要求:
|
||
|
||
| 服务 | 变量 | 当前 Compose 值 |
|
||
| --- | --- | --- |
|
||
| `creator-hub` | `LISTEN_ADDR` | 默认 `:8080` |
|
||
| `creator-hub` | `WEB_DIR` | 镜像内固定为 `/app/web` |
|
||
| `creator-hub` | `DATABASE_URL` | `postgres://creatorhub@postgres/creatorhub?sslmode=disable` |
|
||
| `creator-hub` | `LOG_LEVEL` | 默认 `info` |
|
||
| `creator-hub` | `CONTROL_PLANE_USERNAME` | 必填;HTTP Basic Auth 用户名 |
|
||
| `creator-hub` | `CONTROL_PLANE_PASSWORD` | 必填且至少 6 字节;不会写入日志或响应 |
|
||
| `creator-hub` | `CREATORHUB_CREDENTIAL_MASTER_KEY` | 必填;解密独立凭据卷,不写入数据库、日志或响应 |
|
||
| `creator-hub` | `CREATORHUB_CREDENTIAL_STORE_DIR` | 默认 `/var/lib/creatorhub/credentials`;必须为绝对路径且持久可写 |
|
||
| `creator-hub` | `BAILIAN_API_KEY` | 可选;仅用于已批准的文本 AI,缺失时不回退 Mock |
|
||
| `creator-hub` | `BAILIAN_BASE_URL` | 可选;HTTP(S) 供应商地址,禁止携带用户信息 |
|
||
| `docker-gateway` | `LISTEN_ADDR` | 默认 `:8081` |
|
||
| `docker-gateway` | `DOCKER_SOCKET` | 默认值和 Compose 挂载均固定为 `/var/run/docker.sock`;不能只覆盖环境变量 |
|
||
| `docker-gateway` | `BROWSER_NETWORK` | `creatorhub_browser` |
|
||
| `docker-gateway` | `GATEWAY_TOKEN` | 与平台注册值一致,长度 ≥16;`/v1` 全部接口校验 Bearer 令牌 |
|
||
| `docker-gateway` | `BROWSER_CDP_URL` | 空;仅本地联调时连接外部 CDP,例如 `http://host.docker.internal:9222` |
|
||
| `docker-gateway` | `BROWSER_CDP_TARGET_ID` | 空;外部 CDP 多页面时必须指定抖音页面 ID |
|
||
| `docker-gateway` | `LOG_LEVEL` | 默认 `info` |
|
||
|
||
不要把凭据写入仓库或 Compose 文件。
|
||
|
||
### 本地 9222 CDP 联调
|
||
|
||
网关支持显式连接已经登录的本地 CDP,不创建或回收 Docker 浏览器。仅用于开发和真实平台链路测试,不应在生产 Compose 中设置。先从 `http://127.0.0.1:9222/json/list` 选择唯一的抖音页面 `id`,再在宿主机启动网关:
|
||
|
||
```bash
|
||
export LISTEN_ADDR=127.0.0.1:18081
|
||
export GATEWAY_TOKEN=0123456789abcdef
|
||
export BROWSER_CDP_URL=http://127.0.0.1:9222
|
||
export BROWSER_CDP_TARGET_ID=<已登录抖音页面的 target id>
|
||
export BROWSER_CDP_ALIAS=local-cdp
|
||
export BROWSER_CDP_NETWORK_ID=local-cdp
|
||
python3 -m cmd.docker_gateway.gateway
|
||
```
|
||
|
||
该模式的 generation 固定为 `binding_version=1`、`runtime_id=64 个 0`、`network_id=local-cdp`。生命周期接口明确不可用于此浏览器;身份、受限读取、事件订阅和动作接口仍要求完整 generation,并继续执行 UID 核对。`BROWSER_CDP_TARGET_ID` 必须固定到抖音页面,不能把包含多个网站的 CDP 页面列表交给自动猜测。
|
||
|
||
### P0-lite 停机通知
|
||
|
||
当前只使用 `creator-hub` 的专用 Logrus JSON logger 作为通知渠道;它固定输出警告,不继承业务 `LOG_LEVEL`。选择它是因为 Compose 已可靠收集服务日志,不需要新增外部账号、凭据、网络重试或通知依赖。仅 `policy_hold` 和 `needs_confirmation` 会产生 `operator attention required`,字段限定为 `event_type`、`reason_code`、账号/任务 ID;不会包含请求头、Secret 引用或凭据值。运维可用下列命令接入现有日志采集或人工查看:
|
||
|
||
```bash
|
||
docker compose logs creator-hub | grep 'operator attention required'
|
||
```
|
||
|
||
该渠道是单实例 P0-lite 能力,不保证外部送达、升级或确认回执;只有出现明确的多渠道/送达需求时才增加 webhook 或消息平台。
|
||
|
||
## 更新与回滚
|
||
|
||
更新前记录当前版本并备份数据库:
|
||
|
||
```bash
|
||
set -Eeuo pipefail
|
||
git rev-parse HEAD
|
||
umask 077
|
||
export BACKUP_FILE="$(pwd)/creatorhub-$(date +%Y%m%d-%H%M%S).dump"
|
||
docker compose exec -T postgres \
|
||
pg_dump -U creatorhub -d creatorhub --format=custom \
|
||
> "$BACKUP_FILE"
|
||
test -s "$BACKUP_FILE"
|
||
docker compose exec -T postgres pg_restore --list \
|
||
< "$BACKUP_FILE" >/dev/null
|
||
```
|
||
|
||
PostgreSQL dump 之外,发布备份必须同时包含 credentials、materials 和动态 Profile 卷;这些归档存放在部署侧受限目录,不放进仓库或共享聊天。示例(`BACKUP_DIR` 使用独立磁盘或 Secret Manager 的受控挂载目录):
|
||
|
||
```bash
|
||
set -Eeuo pipefail
|
||
umask 077
|
||
BACKUP_DIR=/srv/creatorhub-backups/$(date +%Y%m%d-%H%M%S)
|
||
mkdir -p "$BACKUP_DIR/profiles"
|
||
# PostgreSQL dump
|
||
pg_dump_file="$BACKUP_DIR/creatorhub.dump"
|
||
docker compose exec -T postgres pg_dump -U creatorhub -d creatorhub --format=custom > "$pg_dump_file"
|
||
test -s "$pg_dump_file"
|
||
# Credential and material volumes
|
||
for volume in creatorhub_credentials creatorhub_materials; do
|
||
docker run --rm -v "$volume:/data:ro" -v "$BACKUP_DIR:/backup" alpine:3.22 \
|
||
tar czf "/backup/${volume}.tar.gz" -C /data .
|
||
done
|
||
# Dynamic browser Profile volumes; absence is an explicit empty set.
|
||
for volume in $(docker volume ls -q --filter name='^creatorhub-profile-'); do
|
||
docker run --rm -v "$volume:/data:ro" -v "$BACKUP_DIR/profiles:/backup" alpine:3.22 \
|
||
tar czf "/backup/${volume}.tar.gz" -C /data .
|
||
done
|
||
find "$BACKUP_DIR" -type f -exec sha256sum {} + > "$BACKUP_DIR/SHA256SUMS"
|
||
```
|
||
|
||
`CREATORHUB_CREDENTIAL_MASTER_KEY` 不写入上述归档;部署管理员必须在独立的 Secret Manager/OS Keyring 保留一份受访问控制的密钥托管记录,并确认恢复主机能注入同一值。恢复前核对 `SHA256SUMS`、主密钥记录和备份目录权限;恢复后在隔离 Compose 项目中还原四类卷,检查账号凭据可解密、材料文件可读、Profile 可挂载,再切换服务。任何一类缺失都判定为恢复失败,不把应用健康检查当作数据恢复证据。
|
||
|
||
拉取已审核版本后,重新执行部署和验证:
|
||
|
||
```bash
|
||
set -Eeuo pipefail
|
||
git pull --ff-only
|
||
export DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)"
|
||
export CREATORHUB_PORT=8080
|
||
docker compose config --quiet
|
||
docker compose up --detach --build
|
||
```
|
||
|
||
镜像版本升级在页面「运行环境 → 升级」完成:删除旧容器并保留 Profile,用新镜像与原指纹/binding 重建,账号可运行才启动,否则保持停止态;无自动回滚。失败先查看记录,人工重试由现有流程调和,不承诺所有失败都可直接重试消除。当前 upgrade 无条件要求有效代理出口,直连环境会失败;rebind 也不支持空出口切回直连。此为现状限制,不是新产品范围裁决。
|
||
|
||
数据库迁移只支持安全前进,不提供自动破坏性回滚。需要同时恢复旧代码和更新前数据库时,修改下面两个变量后**整块执行一次**;不要逐行或拆块执行。预检、恢复演练、动态容器停止、停服、主库恢复、提交切换和启动都位于同一个 fail-fast subshell 中。
|
||
|
||
```bash
|
||
(
|
||
set -Eeuo pipefail
|
||
|
||
BACKUP_FILE=/absolute/path/to/creatorhub-YYYYmmdd-HHMMSS.dump
|
||
RESTORE_REV=PREVIOUS_REVIEWED_COMMIT_SHA
|
||
: "${BACKUP_FILE:?set BACKUP_FILE to the absolute archive path}"
|
||
: "${RESTORE_REV:?set RESTORE_REV to the previous reviewed commit SHA}"
|
||
test -r "$BACKUP_FILE"
|
||
test -s "$BACKUP_FILE"
|
||
git cat-file -e "${RESTORE_REV}^{commit}"
|
||
docker compose exec -T postgres pg_restore --list \
|
||
< "$BACKUP_FILE" >/dev/null
|
||
|
||
docker compose exec -T postgres \
|
||
dropdb --if-exists --force -U creatorhub creatorhub_restore_check
|
||
docker compose exec -T postgres \
|
||
createdb -U creatorhub creatorhub_restore_check
|
||
docker compose exec -T postgres \
|
||
pg_restore -U creatorhub -d creatorhub_restore_check \
|
||
--exit-on-error --no-owner --no-privileges \
|
||
< "$BACKUP_FILE"
|
||
docker compose exec -T postgres \
|
||
psql -U creatorhub -d creatorhub_restore_check -v ON_ERROR_STOP=1 -tAc \
|
||
'SELECT 1 FROM schema_migration WHERE version = 32;' \
|
||
| grep -qx 1
|
||
docker compose exec -T postgres \
|
||
psql -U creatorhub -d creatorhub_restore_check -v ON_ERROR_STOP=1 -tAc \
|
||
'SELECT 1 FROM schema_migration WHERE version = 33;' \
|
||
| grep -qx 1
|
||
docker compose exec -T postgres \
|
||
dropdb --force -U creatorhub creatorhub_restore_check
|
||
|
||
list_running_browsers() {
|
||
docker ps --quiet \
|
||
--filter 'name=^creatorhub-browser-' \
|
||
--filter 'label=io.creatorhub.managed=true' \
|
||
--filter 'label=io.creatorhub.runtime-id'
|
||
}
|
||
|
||
browser_ids="$(list_running_browsers)" || exit 1
|
||
for browser_id in $browser_ids; do
|
||
if ! docker stop "$browser_id"; then
|
||
docker rm --force "$browser_id"
|
||
fi
|
||
done
|
||
|
||
browser_ids="$(list_running_browsers)" || exit 1
|
||
test -z "$browser_ids"
|
||
docker compose stop creator-hub docker-gateway
|
||
|
||
umask 077
|
||
PRE_ROLLBACK_BACKUP="$(pwd)/creatorhub-pre-rollback-$(date +%Y%m%d-%H%M%S).dump"
|
||
docker compose exec -T postgres \
|
||
pg_dump -U creatorhub -d creatorhub --format=custom \
|
||
> "$PRE_ROLLBACK_BACKUP"
|
||
test -s "$PRE_ROLLBACK_BACKUP"
|
||
docker compose exec -T postgres pg_restore --list \
|
||
< "$PRE_ROLLBACK_BACKUP" >/dev/null
|
||
|
||
test -r "$BACKUP_FILE"
|
||
test -s "$BACKUP_FILE"
|
||
git cat-file -e "${RESTORE_REV}^{commit}"
|
||
docker compose exec -T postgres pg_restore --list \
|
||
< "$BACKUP_FILE" >/dev/null
|
||
|
||
docker compose exec -T postgres \
|
||
dropdb --if-exists --force -U creatorhub creatorhub
|
||
docker compose exec -T postgres createdb -U creatorhub creatorhub
|
||
docker compose exec -T postgres \
|
||
pg_restore -U creatorhub -d creatorhub \
|
||
--exit-on-error --no-owner --no-privileges \
|
||
< "$BACKUP_FILE"
|
||
docker compose exec -T postgres \
|
||
psql -U creatorhub -d creatorhub -v ON_ERROR_STOP=1 -tAc \
|
||
'SELECT 1 FROM schema_migration WHERE version = 32;' \
|
||
| grep -qx 1
|
||
docker compose exec -T postgres \
|
||
psql -U creatorhub -d creatorhub -v ON_ERROR_STOP=1 -tAc \
|
||
'SELECT 1 FROM schema_migration WHERE version = 33;' \
|
||
| grep -qx 1
|
||
|
||
git switch --detach "$RESTORE_REV"
|
||
if ! docker compose up --detach --build; then
|
||
docker compose stop creator-hub docker-gateway || true
|
||
exit 1
|
||
fi
|
||
)
|
||
```
|
||
|
||
任何命令失败时 subshell 立即退出;若失败发生在主库 `dropdb` 之后,应用保持停止。`docker compose up` 自身失败时也会显式停回应用服务。成功后重新执行“部署验证”。不要直接删除数据卷或手工改写迁移记录。
|
||
|
||
## 停止与数据保留
|
||
|
||
`docker compose stop/down` 不管理网关动态创建的浏览器容器;直接执行会留下运行中的浏览器和活动会话。先严格按固定容器名前缀及两个管理标签筛选并停止;只有单个容器停止失败或超时时才强制删除。命令不带 `--volumes`,因此 Profile 卷仍会保留:
|
||
|
||
```bash
|
||
set -Eeuo pipefail
|
||
|
||
list_running_browsers() {
|
||
docker ps --quiet \
|
||
--filter 'name=^creatorhub-browser-' \
|
||
--filter 'label=io.creatorhub.managed=true' \
|
||
--filter 'label=io.creatorhub.runtime-id'
|
||
}
|
||
|
||
browser_ids="$(list_running_browsers)" || exit 1
|
||
for browser_id in $browser_ids; do
|
||
if ! docker stop "$browser_id"; then
|
||
docker rm --force "$browser_id"
|
||
fi
|
||
done
|
||
|
||
browser_ids="$(list_running_browsers)" || exit 1
|
||
test -z "$browser_ids"
|
||
|
||
docker compose stop
|
||
```
|
||
|
||
恢复服务使用 `docker compose up --detach`。需要删除服务容器和网络但保留数据时使用:
|
||
|
||
```bash
|
||
docker compose down
|
||
```
|
||
|
||
不要执行 `docker compose down --volumes`:它会删除 PostgreSQL 数据卷。浏览器 Profile 卷不属于 Compose 声明卷,删除浏览器容器或执行 `docker compose down` 时仍会保留;可用以下命令核对:
|
||
|
||
```bash
|
||
docker volume ls --filter name=creatorhub-profile-
|
||
```
|
||
|
||
`docker.sock` 即使以只读文件方式挂载,也仍允许 Docker API 写操作,等价于宿主机 root 权限。完整安全边界、API 契约和失败语义见[《浏览器容器控制面》](architecture/container-control.md)。
|