> 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
# 在服务器上，用显式 token 运行核心。
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"
```

诸如以下的 Vite 回环来源： `http://127.0.0.1:1420` 以及 `http://localhost:1420` 会自动获准。公共 `http://` 来源不建议使用，因为每次 RPC 调用都会携带持有者令牌。

***

## 持有者令牌的唯一事实来源

每个 `/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` 读取该文件。

**对于任何远程 / 容器化部署的经验法则：始终设置 `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     # prints 'env' or '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`。生成方式： `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`，受持有者令牌保护的 JSON-RPC 入口。
* `GET /events`, `GET /ws/dictation`，公开流式通道。

这个 `OPENHUMAN_WORKSPACE` 目录（`/home/openhuman/.openhuman` 在容器内）保存核心的配置、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 负责 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        # tracks the latest prod cut
docker pull ghcr.io/tinyhumansai/openhuman-core:v1.2.4        # pinned by GitHub Release tag
docker pull ghcr.io/tinyhumansai/openhuman-core:1.2.4         # pinned by SemVer
```

该镜像为 `linux/amd64`。arm64 主机请下载同一 GitHub Release 附带的独立 tar 包（`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 tar 包 [GitHub Release](https://github.com/tinyhumansai/openhuman/releases/latest):

```bash
# 选择与主机架构匹配的 tar 包。
ARCH="$(uname -m)"
case "$ARCH" in
  x86_64)  TARGET=x86_64-unknown-linux-gnu  ;;
  aarch64) TARGET=aarch64-unknown-linux-gnu ;;
  *) echo "Unsupported arch: $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`.

如果下载或暂存失败，正在运行的二进制会保留在原处，并且不会请求重启。如果暂存的二进制在重启后证明有问题，请通过包管理器、镜像标签或发布产物恢复到之前的二进制，然后再次重启 supervisor。

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
```

### 轮换持有者令牌

`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，在其中也这样做 **Settings → App-Level Environment Variables**: 编辑 `OPENHUMAN_CORE_TOKEN` secret 并让 App Platform 重新部署。无需删除单独的 token 文件；env var 是唯一的状态。

### 将其置于 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=<你在服务器上设置的相同 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=<你在服务器上设置的相同 token>
```

重启桌面应用。中的提供者链 `App.tsx` 将把所有 RPC 调用路由到远程 core；其他都不变。公共 `http://` 宿主会被应用选择器拒绝；对于任何可公开访问的 core，请使用 HTTPS。

### 故障排查：在云运行时上 OAuth 之后登录失败

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

根本原因几乎总是 **远程** core，而不是桌面端。 `auth_store_session` 让 core 在持久化之前先把新的 session token 与后端进行验证（`GET /auth/me`）；在云运行时中，这个调用是在你的服务器上执行的，所以如果远程 core 无法连接/认证后端，它就会失败：

* **`BACKEND_URL` 未设置或错误。** 这是必需的（见上面的环境变量表），并且必须是 `https://api.tinyhumans.ai` 用于生产环境（或 staging URL）。缺少该值是最常见的原因——有一位报告者的失败原因正是如此。
* **后端从服务器上不可达** （出站防火墙、DNS、TLS 拦截）→ core 在 `/auth/me`.
* **过时的 core。** 较旧的 core 早于 `allowPendingBackendValidation` 并且在没有宽限期的情况下同步验证；请将服务器更新到当前版本。
* **错误的 RPC token。** token 不匹配会在 `/rpc`上表现为 HTTP 401；请重新粘贴 token（见“轮换 bearer token”）。

检查远程 core 日志中的 `Session validation failed (GET /auth/me)` 以及状态/原因。现在桌面端会针对这些情况报告云端特定、可操作的消息，而不是通用的“请重试”（issue #3025）。

***

## 命名卷所有权与 Docker entrypoint

Docker 默认创建的命名卷归 `root:root` 所有。由于 core 以非 root 的 `openhuman` 用户（UID 10001）运行，横幅后第一次写入（`init_rpc_token → write_token_file` 到 `$OPENHUMAN_WORKSPACE`）如果没有先修复所有权，就会抛出 `Permission denied (os error 13)` 。

该镜像带有一个专用 entrypoint，位于 `/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` 。

该 entrypoint 名为 `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)提供了一个模板。填写 `<你的应用名称>` 以及 `<你的区域>` 为你在 `fly launch`:

```toml
app = '<你的应用名称>'
primary_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 <你的区域> --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`的值。之后连接桌面应用时会用到它。 **任何拥有这个 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://<你的应用名称>.fly.dev/health
```

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

在 `app/.env.local`:

```bash
OPENHUMAN_CORE_RUN_MODE=external
OPENHUMAN_CORE_RPC_URL=https://<你的应用名称>.fly.dev/rpc
OPENHUMAN_CORE_TOKEN=<你在步骤 4 中设置的 token>
```

或者使用 **首次运行选择器** 在桌面应用中（Core RPC URL + token 字段旁有一个 **测试连接** 按钮）进行配置，无需编辑文件。

### 持续部署

要在每次推送到 `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 固定到带标签的 release（或者完整的 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` 生成一个部署 token，并将其作为名为 `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` 不会运行 entrypoint，因此会以 root 身份落地并留下一个 `root:root` `config.toml` （core 会以 0600 权限写入它，所以运行时用户之后根本无法打开它）。

`scripts/docker-entrypoint-core.sh` 现在会自动修复此问题：它会在每次启动时递归地 **chown 工作区，** 跳过已经正确归属的条目。如果修复无法运行—— `cap_drop: ALL` 在没有 `cap_add: CHOWN` 的情况下——entrypoint 会拒绝启动，而不是启动一个会对 `/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`），在那里 token 总是预先设置好，并且不使用持久化卷。
* **`docker-volume-permissions`**: 省略 `OPENHUMAN_CORE_TOKEN` 并在 `/home/openhuman/.openhuman`处挂载一个全新的匿名卷。重现 issue #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 .

# token 已设置路径（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

# 新卷 / 无 token 路径（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
```
