> For the complete documentation index, see [llms.txt](https://tinyhumans.gitbook.io/openhuman/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://tinyhumans.gitbook.io/openhuman/zh/gong-neng/cloud-deploy.md).

# 云端部署

OpenHuman 是一款桌面应用，但其 **Rust 核心** (`openhuman-core`) 是可托管于云端的无头 JSON-RPC 服务器。单独部署核心适用于：

* 多设备访问，让多个桌面客户端指向同一个托管核心
* 没有本地 Rust 工具链的内部测试人员
* 应当在笔记本会话结束后仍持续运行的长期 cron 任务 / Webhook

本指南涵盖四种部署路径，按易用程度排序：

1. [DigitalOcean App Platform：一键部署](#1-digitalocean-app-platform-one-click)
2. [DigitalOcean App Platform：通过 doctl 手动部署](#2-digitalocean-app-platform-manual-via-doctl)
3. [通过 Docker Compose 部署至任意 VPS](#3-any-vps-via-docker-compose)
4. [Fly.io](#4-flyio)

每种路径部署的内容：运行以下命令的单个容器 `openhuman-core serve` 在端口 `7788`上。公共主机应位于服务提供商的 TLS 后方，例如 `https://core.example.com/rpc`。仅限私有的 localhost、RFC1918 网络或 Tailscale 等 tailnet 上的主机，在核心无法从公网访问时，可使用纯 HTTP，例如 `http://100.x.x.x:7788/rpc`，。桌面应用已经知道如何与远程核心通信；请设置 `OPENHUMAN_CORE_RPC_URL` 和 `OPENHUMAN_CORE_TOKEN=...` 在 `app/.env.local` 中，然后启动。

***

## 远程 UI 选项

OpenHuman 支持的远程部署方式为 **核心远程，UI 本地**：运行 `openhuman-core` 于 Linux 服务器上，并让桌面客户端指向该 RPC URL。部署后的核心尚不会以生产级 Web 应用的形式提供完整的 React/Tauri UI。仅桌面端功能仍需要 Tauri 外壳，包括托盘控件、原生深度链接、CEF 账户扫描器、操作系统钥匙串集成以及窗口/屏幕功能。

若当前需要在私有服务器上提供可通过浏览器访问的 UI，请使用 Vite Web 构建作为面向远程核心的开发/预览界面：

```bash
# 在服务器上，使用显式令牌运行核心。
export OPENHUMAN_CORE_HOST=0.0.0.0
export OPENHUMAN_CORE_PORT=7788
export OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)"
openhuman-core serve

# 在服务器的另一个 shell 中，仅在回环地址提供 UI。
pnpm --dir app dev -- --host 127.0.0.1 --port 1420
```

然后从工作站隧道转发这两个端口：

```bash
ssh -L 1420:127.0.0.1:1420 -L 7788:127.0.0.1:7788 user@server
```

打开 `http://127.0.0.1:1420`，在首次运行界面中选择远程/核心选项，并输入 `http://127.0.0.1:7788/rpc` 以及 `OPENHUMAN_CORE_TOKEN` 在服务器上的值。

如果从非回环来源提供浏览器 UI，请将该确切来源添加至核心的 CORS 允许列表：

```bash
export OPENHUMAN_CORE_ALLOWED_ORIGINS="https://openhuman-ui.example.com"
```

诸如 `http://127.0.0.1:1420` 和 `http://localhost:1420` 之类的回环 Vite 来源会被自动允许。公共 `http://` 来源不建议使用，因为每次 RPC 调用都会携带 Bearer 令牌。

***

## Bearer 令牌的唯一事实来源

每次 `/rpc` 调用都会携带 `Authorization: Bearer <token>`。核心在启动时有两种加载该令牌的方式（[`src/core/auth.rs`](https://github.com/tinyhumansai/openhuman/tree/main/src/core/auth.rs)):

1. **`OPENHUMAN_CORE_TOKEN` 环境变量**：由调用方（Tauri 外壳、Docker、App Platform、systemd 单元，……）预先设置。核心会原样使用此值，并且 **绝不** 写入文件。
2. **`{workspace}/core.token` 文件**：由核心在首次启动时生成 *仅当 `OPENHUMAN_CORE_TOKEN` 未设置时*。独立的 `openhuman core run` 会使用此方式，因此 CLI 客户端可以 `cat` 该文件。

**任何远程 / Docker 化部署的经验法则：始终设置 `OPENHUMAN_CORE_TOKEN`.** 不要依赖 `core.token` 位于容器中。临时文件系统会在重新部署时丢失它，而且任何尝试从容器外读取该文件的客户端都会得到过期或空的值。这两种路径在启动时被刻意设计为互斥；混用它们是导致“重新部署后仪表板收到 401”的最常见原因。

要检查 *正在运行的* 核心使用的内容，请运行 [`scripts/print-core-token.sh`](https://github.com/tinyhumansai/openhuman/tree/main/scripts/print-core-token.sh) 在主机上运行（或在容器内使用 `docker compose exec`):

```bash
scripts/print-core-token.sh --where     # 输出 'env' 或 'file:/path'
scripts/print-core-token.sh --redact    # 前 8 个十六进制字符 + '…'（可安全用于日志）
scripts/print-core-token.sh             # 完整值（直接管道传入客户端）
```

桌面应用的首次运行选择器还提供了一个 **测试连接** 按钮，位于核心 RPC URL 和令牌字段旁；它会使用输入的令牌对该 URL 发起 `core.ping` 请求，并报告 `已连接 ✓` / `认证失败` / `无法连接` ，然后再内联保存配置。

***

## 开始前所需内容

| 设置                     | 必需 | 说明                                                                               |
| ---------------------- | -- | -------------------------------------------------------------------------------- |
| `OPENHUMAN_CORE_TOKEN` | 是  | 客户端发送至 `/rpc`的 Bearer 令牌。使用以下命令生成： `openssl rand -hex 32`. **任何拥有此令牌的人都可以控制核心。** |
| `BACKEND_URL`          | 是  | 核心所连接的 Tinyhumans 后端（`https://api.tinyhumans.ai` 用于生产环境）。                        |
| `OPENHUMAN_APP_ENV`    | 否  | `production` 或 `staging`。默认为 `production`.                                       |
| `OPENHUMAN_CORE_HOST`  | 否  | 默认为 `0.0.0.0` 在容器中。                                                              |
| `OPENHUMAN_CORE_PORT`  | 否  | 默认为 `7788`.                                                                      |
| `RUST_LOG`             | 否  | `info` 即可； `debug` 用于排查问题。                                                       |

运行中容器暴露的端点：

* `GET /health`，公共存活探针。每种部署路径的健康检查都会使用它。
* `POST /rpc`，受 Bearer 令牌保护的 JSON-RPC 入口点。
* `GET /events`, `GET /ws/dictation`，公共流式通道。

该 `OPENHUMAN_WORKSPACE` 目录（`/home/openhuman/.openhuman` 在容器内）保存核心的配置、sqlite 数据库和技能状态。 **将其挂载到持久卷上** ，用于每个生产部署，否则重启时会丢失数据。

***

## 1. DigitalOcean App Platform：一键部署

点击下方按钮，根据此仓库的以下文件创建新的 App Platform 应用程序： [`.do/app.yaml`](https://github.com/tinyhumansai/openhuman/tree/main/.do/app.yaml):

[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/tinyhumansai/openhuman/tree/main)

然后，在 App Platform UI 中， **在首次部署完成之前**:

1. 打开 **设置 → 应用级环境变量** 选项卡。
2. 替换占位符 `OPENHUMAN_CORE_TOKEN` 值为高强度密钥（`openssl rand -hex 32`）。将其标记为加密。
3. 如果部署的是暂存环境，请将 `OPENHUMAN_APP_ENV` 改为 `staging` 和 `BACKEND_URL` 改为 `https://staging-api.tinyhumans.ai`.
4. 点击 **保存**。App Platform 将使用新密钥重新部署。

App Platform 负责 TLS、崩溃后重启、日志流以及在以下操作时滚动重新部署： `git push` （设置 `deploy_on_push: true` 在 `.do/app.yaml` 以选择启用）。

> **持久化说明：** App Platform Basic 不提供块存储。核心工作区位于容器的临时文件系统中，重新部署时会丢失。若需要持久存储，请附加托管数据库或升级到支持卷的套餐。请参阅 [Compose 路径](#3-any-vps-via-docker-compose) ，了解开箱即用持久卷的自托管替代方案。

***

## 2. DigitalOcean App Platform：通过 doctl 手动部署

如果不想在 UI 中逐步点击：

```bash
# 一次性操作：安装 doctl 并进行认证。
doctl auth init

# 编辑 .do/app.yaml - 将 OPENHUMAN_CORE_TOKEN 设置为真实值（或通过 --spec 搭配 envsubst 在
# 创建时传入）。然后：
doctl apps create --spec .do/app.yaml

# 监视构建：
doctl apps list
doctl apps logs <app-id> --type build --follow
```

编辑 spec 后更新现有应用：

```bash
doctl apps update <app-id> --spec .do/app.yaml
```

***

## 3. 通过 Docker Compose 部署至任意 VPS

适用于任何安装 Docker Engine ≥ 24 及 Compose 插件的主机：DigitalOcean Droplet、Hetzner、Linode、EC2 或家庭服务器。

每个生产版本都会向 GHCR 发布带有多个标签的镜像：

```bash
docker pull ghcr.io/tinyhumansai/openhuman-core:latest        # 跟踪最新的生产版本
docker pull ghcr.io/tinyhumansai/openhuman-core:v1.2.4        # 由 GitHub Release 标签固定
docker pull ghcr.io/tinyhumansai/openhuman-core:1.2.4         # 由 SemVer 固定
```

该镜像为 `linux/amd64`。arm64 主机请拉取附加于同一 GitHub Release 的独立 tarball（`openhuman-core-<version>-aarch64-unknown-linux-gnu.tar.gz`），或在 arm64 构建器上从源代码构建镜像。

使用已发布镜像快速运行：

```bash
docker run -d --name openhuman-core -p 7788:7788 \
  -e OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)" \
  -e BACKEND_URL=https://api.tinyhumans.ai \
  -e OPENHUMAN_APP_ENV=production \
  -v openhuman-workspace:/home/openhuman/.openhuman \
  ghcr.io/tinyhumansai/openhuman-core:latest
```

或者使用仓库内的 Compose 文件（仍会从以下文件在本地构建镜像： `Dockerfile`；切换 `image:` 字段为 `ghcr.io/tinyhumansai/openhuman-core:latest` 在 `docker-compose.yml` ，即可改用已发布的镜像）：

```bash
# 在服务器上：
git clone https://github.com/tinyhumansai/openhuman.git
cd openhuman

# 配置密钥：
cp .env.example .env
# 编辑 .env - 至少设置：
#   BACKEND_URL=https://api.tinyhumans.ai
#   OPENHUMAN_CORE_TOKEN=<openssl rand -hex 32>
#   OPENHUMAN_APP_ENV=production

# 构建并启动：
docker compose up -d

# 验证：
docker compose ps
curl -fsS http://localhost:7788/health
```

### 无需 Docker 的无头安装

如果主机无法运行 Docker，请获取附加于最新版本的独立 CLI tarball： [GitHub Release](https://github.com/tinyhumansai/openhuman/releases/latest):

```bash
# 选择与主机架构相匹配的 tarball。
ARCH="$(uname -m)"
case "$ARCH" in
  x86_64)  TARGET=x86_64-unknown-linux-gnu  ;;
  aarch64) TARGET=aarch64-unknown-linux-gnu ;;
  *) echo "不支持的架构：$ARCH"; exit 1 ;;
esac
VERSION=1.2.4   # 设置为所需的发行版本
curl -fsSL "https://github.com/tinyhumansai/openhuman/releases/download/v${VERSION}/openhuman-core-${VERSION}-${TARGET}.tar.gz" \
  | tar -xz -C /usr/local/bin
openhuman-core --version
```

然后运行 `openhuman-core serve` ，使用您选择的服务管理器（systemd、supervisord，……）并设置与上文相同的环境变量。

### 无头自更新约定

无头部署应将 `openhuman.update_apply` 视为安全原语：它下载发行资产，将其原子地写入当前二进制文件旁边，然后返回。不会自动退出任何进程。

`openhuman.update_run` 遵循 `config.update.restart_strategy`:

* `self_replace` （默认）：暂存二进制文件，发布进程内重启请求，并让运行中的核心自行重新生成进程。
* `supervisor`：暂存二进制文件并返回 `restart_requested=false`。外部服务管理器必须重启该进程。

对于长期运行的 Linux 服务，请设置：

```toml
[update]
restart_strategy = "supervisor"
rpc_mutations_enabled = false
```

或等效的环境变量：

```bash
OPENHUMAN_AUTO_UPDATE_RESTART_STRATEGY=supervisor
OPENHUMAN_AUTO_UPDATE_RPC_MUTATIONS_ENABLED=false
```

推荐的 `systemd` 配置：

```ini
Restart=always
ExecReload=/bin/kill -HUP $MAINPID
```

运维流程：

1. 调用 `openhuman.update_check` 以发现发行版本。
2. 配置 `restart_strategy = "supervisor"` 在您的 `update.toml` 中（或设置 `OPENHUMAN_AUTO_UPDATE_RESTART_STRATEGY=supervisor`），使核心暂存新二进制文件而不尝试自行重新执行，然后调用 `openhuman.update_apply` 或 `openhuman.update_run`. `restart_strategy` 是一个配置设置，而非 RPC 参数。
3. 显式重启该单元： `systemctl restart openhuman`.

如果下载或暂存失败，运行中的二进制文件将保持不变，也不会请求重启。如果暂存的二进制文件在重启后被证明有问题，请从包管理器、镜像标签或发行工件中恢复先前的二进制文件，然后再次重启监督程序以回滚。

Compose 文件（[`docker-compose.yml`](https://github.com/tinyhumansai/openhuman/tree/main/docker-compose.yml)）将核心映射到 `:7788`，挂载名为 `openhuman-workspace` 的卷以实现持久化，并设置 `restart: unless-stopped` ，使核心在主机重启后恢复运行。

### 更新

```bash
git pull
docker compose build
docker compose up -d
```

对于暴露 RPC 的生产部署，建议保持变更性更新 RPC 处于禁用状态（`OPENHUMAN_AUTO_UPDATE_RPC_MUTATIONS_ENABLED=false`），并通过现有的镜像标签或包管理流程执行发布。

### 日志

```bash
docker compose logs -f openhuman-core
```

### 轮换 Bearer 令牌

`OPENHUMAN_CORE_TOKEN` 是公网与完整 RPC 访问权限之间唯一的屏障。请定期轮换，并在怀疑泄露后立即轮换：

```bash
# 1. 生成新令牌并更新服务器端 .env。
openssl rand -hex 32 > /tmp/new-token
sed -i.bak "s|^OPENHUMAN_CORE_TOKEN=.*|OPENHUMAN_CORE_TOKEN=$(cat /tmp/new-token)|" .env
rm /tmp/new-token .env.bak

# 2. 重启容器，使新值传递至核心进程。
docker compose up -d --force-recreate openhuman-core

# 3. 确认运行中的容器正在使用新令牌（已脱敏）。
docker compose exec openhuman-core /bin/sh -c \
  'echo -n "$OPENHUMAN_CORE_TOKEN" | head -c 8; echo "…"'

# 4. 更新每个桌面客户端（切换模式 → 在选择器中重新粘贴，或
# 编辑 app/.env.local 中的 OPENHUMAN_CORE_TOKEN 并重新启动）。仍然
# 持有旧令牌的客户端会在下一次 /rpc 调用时收到 HTTP 401 — 即
# 是预期行为，并非回归。
```

对于 App Platform，也请在以下位置执行相同操作 **设置 → 应用级环境变量**: 编辑 `OPENHUMAN_CORE_TOKEN` 密钥并让 App Platform 重新部署。没有单独的令牌文件需要删除；环境变量是唯一的状态。

### 将其置于 TLS 之后

使用 Caddy、nginx 或 Traefik 作为位于以下服务前方的反向代理 `:7788`。一个最简 `Caddyfile`:

```caddy
core.example.com {
  reverse_proxy localhost:7788
}
```

***

## 将桌面应用指向托管核心

在桌面应用的环境文件中（`app/.env.local`):

```bash
# 使用托管核心，而非启动本地 sidecar。
OPENHUMAN_CORE_RUN_MODE=external
OPENHUMAN_CORE_RPC_URL=https://core.example.com/rpc
OPENHUMAN_CORE_TOKEN=<你在服务器上设置的同一令牌>
```

对于没有公网 IP、仅限私有 tailnet 的 VM，请改用 tailnet URL：

```bash
OPENHUMAN_CORE_RUN_MODE=external
OPENHUMAN_CORE_RPC_URL=http://100.x.x.x:7788/rpc
OPENHUMAN_CORE_TOKEN=<你在服务器上设置的同一令牌>
```

重启桌面应用。以下文件中的提供程序链 `App.tsx` 会将所有 RPC 调用路由至远程核心；其他部分无需变更。公共 `http://` 主机会被应用选择器拒绝；任何可从公网访问的核心都请使用 HTTPS。

### 故障排除：云运行时中 OAuth 后登录失败

症状：浏览器中的 OAuth 完成（“关闭此页面并返回应用”），但桌面端随后显示登录错误——而 **同一** 账户在本地（嵌入式）运行时可正常登录（问题 #3025）。

根本原因几乎总是 **远程** 核心，而不是桌面端。 `auth_store_session` 会使核心在持久化之前针对后端验证新的会话令牌（`GET /auth/me`）；在云运行时中，该调用在你的服务器上运行，因此如果远程核心无法访问/验证后端身份验证，它就会失败：

* **`BACKEND_URL` 未设置或错误。** 它是必需的（参见上方环境变量表），并且必须为 `https://api.tinyhumans.ai` 用于生产环境（或预发布 URL）。缺少该值是最常见的原因——有一位报告者的失败正是如此。
* **服务器无法访问后端** （出口防火墙、DNS、TLS 拦截）→ 核心在以下端点遇到网关错误/超时： `/auth/me`.
* **核心版本过旧。** 旧版核心早于 `allowPendingBackendValidation` ，并且会在没有宽限期的情况下同步验证；请将服务器更新到当前版本。
* **错误的 RPC 令牌。** 令牌不匹配会在以下位置表现为 HTTP 401： `/rpc`；请重新粘贴令牌（参见“轮换 Bearer 令牌”）。

在远程核心日志中检查 `会话验证失败（GET /auth/me）` 及其状态/原因。桌面端现在会针对这些情况报告云端特定且可操作的消息，而不再是笼统的“重试”（问题 #3025）。

***

## 命名卷所有权与 Docker 入口点

Docker 创建的命名卷默认归属 `root:root` 。由于核心以非 root 用户 `openhuman` （UID 10001）运行，因此横幅后的首次写入（`init_rpc_token → write_token_file` 写入 `$OPENHUMAN_WORKSPACE`）会引发 `权限被拒绝（操作系统错误 13）` ，除非先有操作修复所有权。

该镜像在以下位置提供专用入口点： `/usr/local/bin/docker-entrypoint-core.sh` ，其操作如下：

1. 以以下用户启动： `root`.
2. 运行 `mkdir -p` + `chown openhuman:openhuman` 作用于以下两者： `$OPENHUMAN_WORKSPACE` 和 `$HOME/.openhuman` （目录 `core.token` 会在以下变量 `OPENHUMAN_CORE_TOKEN` 未设置时写入）。
3. 调用 `exec gosu openhuman openhuman-core "$@"` 以降权并将控制权交给该二进制文件。

这是 **幂等的**：对于新创建的卷，chown 会修复归 root 所有的目录；对于已修复的卷，chown 不执行任何操作。从早于此修复的镜像升级时，无需手动执行 `docker volume rm` 。

该入口点名为 `docker-entrypoint-core.sh` ，并且 **仅** 连接到根目录 `Dockerfile`。E2E 镜像（`e2e/docker-entrypoint.sh`）不受影响。

***

## 4. Fly.io

[Fly.io](https://fly.io) 非常适合 `openhuman-core`：它会自动处理 TLS，所有套餐均支持持久卷，并且可自动停止空闲机器以降低成本。

### 前提条件

* [flyctl](https://fly.io/docs/flyctl/install/) 已安装并完成认证（`fly auth login`)
* 一个 Fly.io 账户

### 第 1 步：启动应用

```bash
fly launch --no-deploy --config .fly/fly.toml
```

Fly.io 会自动检测 `Dockerfile` 。选择靠近用户的区域，并在提示时跳过首次部署。这会生成一个配置文件。

### 第 2 步：配置 `.fly/fly.toml`

仓库在以下位置提供模板： [`.fly/fly.toml`](https://github.com/tinyhumansai/openhuman/tree/main/.fly/fly.toml)。填入 `<你的应用名称>` 和 `<你的区域>` ，使用你在以下过程中选择的值： `fly launch`:

```toml
app = '<your-app-name>'
primary_region = '<your-region>'

[build]
  dockerfile = "Dockerfile"

[env]
  OPENHUMAN_CORE_HOST = "0.0.0.0"
  OPENHUMAN_CORE_PORT = "7788"
  OPENHUMAN_WORKSPACE = "/home/openhuman/.openhuman"
  RUST_LOG = "info"

[[mounts]]
  source = "openhuman_workspace"
  destination = "/home/openhuman/.openhuman"

[http_service]
  internal_port = 7788
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  # min_machines_running = 0 会在空闲时完全停止机器（成本最低），但
  # 空闲后的第一个请求会承担冷启动代价（容器启动 +
  # Rust 二进制文件初始化——数秒）。设为 1 可保持一台机器处于预热状态。
  min_machines_running = 0
  processes = ['app']

  [[http_service.checks]]
    interval = "30s"
    timeout = "5s"
    grace_period = "10s"
    method = "GET"
    path = "/health"

[[vm]]
  memory = '1gb'
  cpus = 1
```

### 第 3 步：创建持久卷

```bash
fly volumes create openhuman_workspace --size 5 --region <your-region> --config .fly/fly.toml
```

**将工作区挂载在持久卷上** ，否则每次重新部署都会丢失数据。

### 第 4 步：设置密钥

```bash
# 必需
fly secrets set OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)"
fly secrets set BACKEND_URL="https://api.tinyhumans.ai"
fly secrets set OPENHUMAN_APP_ENV="production"

# 建议用于任何可从公网访问的部署：
fly secrets set OPENHUMAN_AUTO_UPDATE_RPC_MUTATIONS_ENABLED="false"
fly secrets set OPENHUMAN_AUTO_UPDATE_RESTART_STRATEGY="supervisor"

# 可选——错误报告和分析：
fly secrets set OPENHUMAN_CORE_SENTRY_DSN="https://<key>@o<org>.ingest.sentry.io/<project>"
fly secrets set OPENHUMAN_ANALYTICS_ENABLED="true"
```

保存以下值： `OPENHUMAN_CORE_TOKEN`。稍后连接桌面应用时将需要它。 **任何持有此令牌的人都可以操控核心**；请像对待密码一样对待它，并在 `fly secrets set OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)"` 发生任何疑似泄露后进行轮换。

### 第 5 步：部署

```bash
fly deploy --config .fly/fly.toml
```

验证核心是否正常：

```bash
curl -fsS https://<your-app-name>.fly.dev/health
```

### 第 6 步：将桌面应用指向托管核心

在 `app/.env.local`:

```bash
OPENHUMAN_CORE_RUN_MODE=external
OPENHUMAN_CORE_RPC_URL=https://<your-app-name>.fly.dev/rpc
OPENHUMAN_CORE_TOKEN=<你在第 4 步设置的令牌>
```

或者使用 **首次运行选择器** ，该选择器位于桌面应用中（包含 Core RPC URL 和令牌字段，以及一个 **测试连接** 按钮），无需编辑文件即可配置。

### 持续部署

若要在每次推送到以下分支时自动重新部署： `main`，请在以下位置添加工作流文件： `.github/workflows/fly-deploy.yml`:

```yaml
name: Fly Deploy
on:
  push:
    branches:
      - main
    paths:
      - 'src/**'
      - 'Cargo.toml'
      - 'Cargo.lock'
      - 'Dockerfile'
      - '.fly/fly.toml'
      - 'scripts/docker-entrypoint-core.sh'
jobs:
  deploy:
    name: 部署 openhuman-core
    runs-on: ubuntu-latest
    concurrency: deploy-group
    steps:
      - uses: actions/checkout@v4
      # 将 Fly action 固定到带标签的版本（或完整的提交 SHA），而不是
      # `@master`——跟踪一个不断变动的分支会信任未来推送到该分支的每一次提交
      #，包括由已被攻破的维护者账户所做的任何提交。
      - uses: superfly/flyctl-actions/setup-flyctl@1.5
      - run: flyctl deploy --remote-only --config .fly/fly.toml
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
```

使用以下命令生成部署令牌： `fly tokens create deploy` ，并将其添加为名为以下名称的仓库密钥： `FLY_API_TOKEN`.

### 更新

```bash
fly deploy --config .fly/fly.toml
```

对于版本固定的部署，请更新以下位置中的镜像标签 `.fly/fly.toml` ，然后重新部署：

```toml
[build]
  image = "ghcr.io/tinyhumansai/openhuman-core:v1.2.4"
```

### 日志

```bash
fly logs --config .fly/fly.toml
```

### 已知陷阱：卷上的 UID 不匹配

如果你在从以下来源构建之间切换： `Dockerfile` （它创建 `openhuman` 用户时使用 UID 10001）以及拉取预构建的 GHCR 镜像（使用 UID 1000），则已写入持久卷的文件将归旧 UID 所有，并在启动时产生 `权限被拒绝（操作系统错误 13）` 。

同样的情况也会发生在镜像升级后仍保留的 Docker 命名卷，以及任何由 **root** `docker exec` 写入的工作区中—— `docker exec` 不会运行入口点，因此会以 root 身份写入，并留下一个 `root:root` `config.toml` （核心以 0600 权限模式写入它，因此运行时用户随后完全无法打开它）。

`scripts/docker-entrypoint-core.sh` 现在会自动修复此问题：它会对工作区执行 **递归地** chown，每次启动时跳过已具有正确所有权的条目。如果无法执行修复—— `cap_drop: ALL` 且未提供 `cap_add: CHOWN` ——入口点会拒绝启动，而不是启动一个对以下端点响应 `/health` 返回 200、但每个配置 RPC 都返回 `权限被拒绝（操作系统错误 13）`的容器，并打印需要运行的确切 `chown` 命令。请在容器日志中查找 `[docker-entrypoint] 预修复` / `致命错误` 行。

通过 SSH 登录并重新设置工作区所有权，以修复旧容器：

```bash
fly ssh console --config .fly/fly.toml
chown -R openhuman:openhuman /home/openhuman/.openhuman/
exit
fly machine restart --config .fly/fly.toml
```

Docker 中的等效操作。请从容器中获取 ID，而不要将其硬编码，这样无论运行哪个镜像都能保持正确：

```bash
docker exec -u 0 openhuman-core sh -c \
  'chown -Rh "$(id -u openhuman):$(id -g openhuman)" /home/openhuman/.openhuman'
docker restart openhuman-core
```

若要在修复前查看各项归哪个 UID 所有，请注意 `docker exec` 默认使用 **root**，因此请明确询问运行时用户，而不要相信裸的 `id`:

```bash
docker exec openhuman-core sh -c 'id openhuman; ls -ln /home/openhuman/.openhuman/config.toml'
```

***

## 冒烟测试

有两种故障模式用于保护云部署路径：

* **`docker-image`**：设置 `OPENHUMAN_CORE_TOKEN` 且不挂载卷。保护 DigitalOcean App Platform 路径（`.do/app.yaml`），其中令牌始终预先设置，且不使用持久卷。
* **`docker-volume-permissions`**：省略 `OPENHUMAN_CORE_TOKEN` 并在以下位置挂载一个新的匿名卷： `/home/openhuman/.openhuman`。复现问题 #2065 的确切故障模式，并断言 `/health` 返回 200，且 `权限被拒绝（操作系统错误 13）` 未出现在日志中。

在本地运行冒烟检查：

```bash
docker build -t openhuman-core:smoke .

# 可选：调整构建配置文件和 Cargo 并行度。
# 在资源受限的构建器上保持 CARGO_BUILD_JOBS=1；在更大的机器上提高该值。
docker build --build-arg CARGO_PROFILE=release --build-arg CARGO_BUILD_JOBS=4 -t openhuman-core:release .

# 已设置令牌的路径（App Platform）：
docker run -d --name oh-smoke -p 7788:7788 \
  -e OPENHUMAN_CORE_TOKEN=smoke-test-token \
  openhuman-core:smoke
curl -fsS http://localhost:7788/health
docker rm -f oh-smoke

# 新卷 / 无令牌路径（Docker Compose、VPS）：
docker volume create oh-vol-test
docker run -d --name oh-vol-smoke -p 7789:7788 \
  -v oh-vol-test:/home/openhuman/.openhuman \
  openhuman-core:smoke
curl -fsS http://localhost:7789/health
docker rm -f oh-vol-smoke
docker volume rm oh-vol-test
```
