> 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-core——可选 DigitalOcean App Platform、Fly.io，或在任意 VPS 上使用 Docker Compose。

OpenHuman 是一个桌面应用，但它的 **Rust 核心** (`openhuman-core`) 是一个无头 JSON-RPC 服务器，可以托管在云端。将 core 单独部署对于以下场景很有用：

* 多设备访问，让多个桌面客户端指向同一个托管的 core
* 没有本地 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`，当 core 无法从公共互联网访问时。桌面应用已经知道如何与远程 core 通信；请设置 `OPENHUMAN_CORE_RPC_URL` 和 `OPENHUMAN_CORE_TOKEN=...` 到 `app/.env.local` 中并启动。

***

## 远程 UI 选项

OpenHuman 支持的远程部署是 **core 远程，UI 本地**：在 `openhuman-core` Linux 服务器上运行，并让桌面客户端指向该 RPC URL。已部署的 core 目前还不会将完整的 React/Tauri UI 作为生产级 Web 应用提供。仅桌面端功能仍然需要 Tauri 外壳，包括托盘控制、原生深度链接、CEF 账号扫描器、OS 密钥串集成，以及窗口/屏幕相关能力。

如果你今天想在私有服务器上获得可通过浏览器访问的 UI，请将 Vite Web 构建作为面向远程 core 的开发/预览界面：

```bash
# 在服务器上，使用显式令牌运行 core。
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`，在首次运行界面中选择远程/core 选项，然后输入 `http://127.0.0.1:7788/rpc` 以及来自服务器的 `OPENHUMAN_CORE_TOKEN` 值。

如果你把浏览器 UI 从非回环 origin 提供出来，请将该确切 origin 添加到 core 的 CORS 白名单：

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

像 `http://127.0.0.1:1420` 和 `http://localhost:1420` 这样的回环 Vite origin 会自动获准。公共的 `http://` origin 不建议使用，因为每次 RPC 调用都会携带 bearer 令牌。

***

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

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

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

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

要检查 *正在运行的* core 正在使用什么，请运行 [`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             # 完整值（可直接通过管道传给客户端）
```

桌面应用的首次运行选择器也提供了一个 **测试连接** 按钮，位于 Core RPC URL + token 字段旁边，它会向 `core.ping` 发起请求，使用输入的令牌访问该 URL，并报告 `已连接 ✓` / `认证失败` / `不可达` ，然后再保存配置。

***

## 开始前你需要准备什么

| 设置                     | 必需 | 备注                                                                                 |
| ---------------------- | -- | ---------------------------------------------------------------------------------- |
| `OPENHUMAN_CORE_TOKEN` | 是  | 客户端发送给 `/rpc`的 Bearer 令牌。使用以下命令生成 `openssl rand -hex 32`. **任何拥有该令牌的人都可以控制 core。** |
| `BACKEND_URL`          | 是  | core 与之通信的 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` 在容器内）保存 core 的配置、sqlite 数据库和技能状态。 **将其挂载到持久卷上** ，否则在每次生产部署重启时你都会丢失数据。

***

## 1. DigitalOcean 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 应用。 **在首次部署完成之前**:

1. 打开 **Settings → App-Level Environment Variables** 选项卡。
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 会在 `git push` 时处理 TLS、崩溃自动重启、日志流式传输和滚动重新部署（ `deploy_on_push: true` 到 `.do/app.yaml` 以启用）。

> **持久化说明：** App Platform Basic 不提供块存储。core 的工作区位于容器的临时文件系统中，并会在重新部署时丢失。若要获得持久存储，请挂载托管数据库或升级到支持卷的套餐。另见 [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，请下载最新 [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` （默认）：暂存二进制，发布进程内重启请求，并让当前运行的 core 自行重启。
* `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`），使 core 在暂存新二进制时不会尝试自我 re-exec，然后调用 `openhuman.update_apply` 或 `openhuman.update_run`. `restart_strategy` 是一个配置项，不是 RPC 参数。
3. 显式重启该单元： `systemctl restart openhuman`.

如果下载或暂存失败，当前运行的二进制会保留原位，也不会请求重启。如果暂存后的二进制在重启后被证实有问题，可以通过从你的包管理器、镜像标签或发布产物中恢复先前二进制并再次重启 supervisor 来回滚。

Compose 文件（[`docker-compose.yml`](https://github.com/tinyhumansai/openhuman/tree/main/docker-compose.yml)）将 core 映射到 `:7788`，挂载一个命名卷 `openhuman-workspace` 用于持久化，并设置 `restart: unless-stopped` ，这样主机重启后 core 会自动恢复。

### 更新

```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. 重启容器，使新值进入 core 进程。
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，也请在其中执行相同的操作 **Settings → App-Level Environment Variables**: 编辑 `OPENHUMAN_CORE_TOKEN` secret，然后让 App Platform 重新部署。没有单独的令牌文件可删；环境变量是唯一的状态。

### 将其置于 TLS 之后

在……前面使用 Caddy、nginx 或 Traefik 作为反向代理 `:7788`。一个最小的 `Caddyfile`:

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

***

## 将桌面应用指向托管的 core

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

```bash
# 使用托管的 core，而不是启动本地 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=<与服务器上设置的相同令牌>
```

重启桌面应用。中的 provider 链会 `App.tsx` 会将所有 RPC 调用路由到远程 core；其他都不会改变。公开的 `http://` 主机会被应用选择器拒绝；任何可公开访问的 core 都应使用 HTTPS。

### 故障排查：在云运行环境中，OAuth 后登录失败

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

桌面 shell 会先在后端验证新的会话令牌（`GET /auth/me`）本身，然后才将凭证交给 core（`auth.set_credential`）。在云运行环境中，这个交接会传到你的服务器，因此常见原因是：

* **RPC 令牌或 URL 错误。** 令牌不匹配会在……上表现为 HTTP 401 `/rpc`；重新粘贴令牌（参见“轮换 bearer 令牌”）并检查 URL。
* **过旧的 core。** 较旧的 core 早于 `auth.set_credential`；桌面端仍会发送旧的 `auth_store_session` 名称通过别名传递，但早于该别名的 core 会拒绝该调用。请将服务器更新到当前版本。
* **`BACKEND_URL` 在服务器上未设置或设置错误。** 远程 core 不再验证会话，但它代表你发出的每个后端调用（计费、团队、托管推理）仍会走那里。它必须是 `https://api.tinyhumans.ai` 用于生产环境（或预发 URL）。

检查桌面日志中的 shell session-owner 行（`[session]`）以及远程 core 日志中的 `[credentials][set-credential]`。对于这些问题，桌面端会报告云环境特定、可执行的消息，而不是笼统的“重试一次”（问题 #3025）。

### 无头登录

云端 core 没有浏览器来完成 OAuth。请在其他地方获取凭证，并在启动时交给 core：

* `OPENHUMAN_BACKEND_API_KEY=<key>` — 一个 TinyHumans API 密钥（推荐；不含用户身份信息，也不会过期）。
* `OPENHUMAN_BACKEND_SESSION_TOKEN=<jwt>` — 一个带有 subject 声明的会话 JWT。

任一方式都只会在存储中没有该类凭证时安装；要轮换，请先清除（`openhuman-core auth clear_credential --kind api-key`）。同样的操作也可以通过 RPC 使用 `openhuman.auth_clear_credential`.

***

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

Docker 创建的命名卷默认归 `root:root` 所有。由于 core 以非 root 的 `openhuman` 用户（UID 10001）运行，因此在横幅之后的第一次写入（`init_rpc_token → write_token_file` 到 `$OPENHUMAN_WORKSPACE`）会触发 `Permission denied (os error 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` 并且仅 **仅** 连接到 root `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)。填写 `<your-app-name>` 和 `<your-region>` ，使用你在 `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`的值。之后连接桌面应用时会用到它。 **任何持有此令牌的人都可以操控 core**；请像对待密码一样对待它，并在以下情况后使用 `fly secrets set OPENHUMAN_CORE_TOKEN="$(openssl rand -hex 32)"` 在怀疑泄露后轮换。

### 步骤 5：部署

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

验证 core 是否健康：

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

### 步骤 6：将桌面应用指向托管的 core

在 `app/.env.local`:

```bash
OPENHUMAN_CORE_RUN_MODE=external
OPENHUMAN_CORE_RPC_URL=https://<your-app-name>.fly.dev/rpc
OPENHUMAN_CORE_TOKEN=<the token you set in Step 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 固定到带标签的发布版（或完整 commit 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 所有，并在启动时产生 `Permission denied (os error 13)` 启动时错误。

同样的事情也会发生在跨镜像升级仍然存在的 Docker 命名卷上，以及任何被 **root** `docker exec` 写入过的工作区上—— `docker exec` 不会运行入口点，因此会以 root 身份写入并留下一个 `root:root` `config.toml` （core 以 0600 模式写入它，因此运行时用户随后根本无法打开它）。

`scripts/docker-entrypoint-core.sh` 现在会自动修复这一点：它会对工作区进行 chown **递归地** 并在每次启动时递归执行，跳过已正确拥有的条目。如果无法执行修复—— `cap_drop: ALL` 没有 `cap_add: CHOWN` ——入口点会拒绝启动，而不是启动一个会对 `/health` 返回 200、但每个配置 RPC 都返回 `Permission denied (os error 13)`错误 `chown` 要运行。请查看 `[docker-entrypoint] pre-heal` / `FATAL` 容器日志中的行。

通过 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，且 `Permission denied (os error 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
```
