Files
creator-hub/docs/deployment.md
T

526 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.sockDOCKER_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)。