自部署(self-hosted)是 MagicFrog 控制面的部署模型。支持两种形态,共享同一条规则:没有托管服务,也没有其他部署拓扑。单文件服务端是主路径——一个下载的二进制、一个 SQLite 文件,不需要容器 runtime。Docker Compose 仍可作为可选替代。
快速开始——单文件服务端
三条命令即可拉起一个可用的控制面:
curl -fsSL https://www.magicfrog.ai/install-server | bash
chmod +x ~/.magicfrog/bin/magicfrog-server
MAGICFROG_PASSWORD='<server-api-password>' \
MAGICFROG_ADMIN_IDENTIFIER='admin@example.com' \
MAGICFROG_ADMIN_PASSWORD='<initial-admin-password>' \
MAGICFROG_FIELD_KEY='<64-hex-chars>' \
./magicfrog-server serve --hostname 0.0.0.0 --port 4096
安装器把二进制放进 ~/.magicfrog/bin——与 magicfrog 客户端 CLI 相同的位置。在该目录中运行,或把它加进你的 PATH;可用 --data <dir> 或 MAGICFROG_DATA_DIR 设置别的数据目录。
必需环境变量
四个变量全部必填——缺任何一个,服务端都会显式启动失败,而不是带着空凭据启动:
| 变量 | 含义 |
|---|---|
MAGICFROG_PASSWORD |
客户端 --server 连接密码 |
MAGICFROG_ADMIN_IDENTIFIER |
初始管理员(邮箱或用户名) |
MAGICFROG_ADMIN_PASSWORD |
初始管理员密码 |
MAGICFROG_FIELD_KEY |
字段加密密钥,64 个十六进制字符(openssl rand -hex 32) |
MAGICFROG_ADMIN_IDENTIFIER 与 MAGICFROG_ADMIN_PASSWORD 必须成对注入——只给其一会让服务端在启动时显式失败。MAGICFROG_FIELD_KEY 也可以改用 MAGICFROG_FIELD_KEY_FILE(卷/secret 挂载)提供;两者都设置时以直接值为准。
首次启动与初始管理员
首次启动时,服务端把一个以给定标识与初始密码的账号写入 SQLite,标记它 role=owner 并置上 must_change_password;账号已存在时跳过写入(重启不会覆盖它)。初始密码只属于部署状态,且一次性。
强制改密
用初始密码经 POST /v1/account/login 登录(/admin 的管理控制台登录是同一道门)。响应携带 mustChangePassword=true。
该标志置位期间,除登录与改密之外的每个业务端点都以 403 password_change_required 拒绝请求。客户端呈现改密界面而不是工作台。经 POST /v1/account/change-password 提交旧密码与新密码:服务端校验旧密码,把新密码存为 argon2id PHC 哈希(明文绝不持久化,也不存在可逆或更弱的第二哈希通道),并清除标志。初始密码就此作废。
范围:一台服务器
控制面刻意做成单服务器:它只在自己所在的主机上、经该主机的容器 runtime 托管创建并管理云电脑。没有远程 Docker daemon 设置(DOCKER_HOST 一类)、没有多主机节点资源池、没有跨机器调度或集群模式。持久化遵循同一条规则——一个 SQLite 文件,没有外部或共享数据库选项。完整边界见云电脑。
你用 CLI 自行登记的机器在这个范围之外:控制面通过它们主动外连的 daemon 连接向它们发送工具调用,但从不托管创建、启动、停止或销毁它们。
单文件服务端的可变状态存放在数据目录(MAGICFROG_DATA_DIR 或 --data;SQLite 数据库为 MAGICFROG_DB):
| 内容 | 位置 |
|---|---|
| SQLite 数据库 | <data>/magicfrog.db(MAGICFROG_DB) |
| 数据 / 状态 / 缓存 / 配置 | <data>/xdg/{data,state,cache,config} |
| CLI 凭据 | <data>/.magicfrog/credentials |
字段加密密钥
MAGICFROG_FIELD_KEY 加密那些服务端仍需按精确值检索的个人数据列:账号 identifier(邮箱或用户名)、enroll 登记的设备名与计算机名。加密使用 AES-256-GCM 加确定性 synthetic nonce,因此登录与查找照常工作,而明文永不落库。它是对称密钥,不是密码哈希:服务端运行时需要它。
- 每个部署单独生成一个:
openssl rand -hex 32(64 个十六进制字符,32 字节)。 - 与数据目录一起存放,不放进镜像:和其他 secret 在同一处 export,或经
MAGICFROG_FIELD_KEY_FILE从你的 secret 存储挂载进来(两者都设置时直接值优先)。 - **密钥缺失、为空、不是 32 字节或文件不可读时,启动失败。**服务端绝不回落成明文存储这些列。这个失败即关闭(fail-closed)的行为是刻意的。
- **密钥丢失,被加密的列无法恢复。**请随数据目录一起备份。
- **轮换密钥需要重新加密。**迁移运行完成之前保留旧密钥可用;不要在切换的瞬间就把它删掉。重加密过程用旧密钥解出每一条存储值、再用新密钥重写;不存在某个值以明文落盘的窗口。以一把解不开盘上既有行的密钥启动的服务端会在启动时失败(它不会返回空结果或错误的密码提示)。
按数据类别的加密形态
每一类敏感值都有唯一确定的存储形态,没有任何一类是明文:
| 数据 | 存储形态 | 可逆 |
|---|---|---|
| 账号密码 | argon2id PHC 哈希 | 否 |
| 会话 access / refresh token | SHA-256 哈希 | 否 |
| 设备 token | 服务端存 SHA-256 哈希 | 否 |
| 配对码(pairing grant) | SHA-256 哈希,按哈希比对 | 否 |
| 账号 identifier、登记设备名、计算机名 | 用字段密钥做 AES-256-GCM(enc:v1: 信封) |
仅在持有 MAGICFROG_FIELD_KEY 时 |
| 设备指纹、已登记能力 | 摘要与能力集合,非个人数据 | 按设计不加密 |
两条边界是刻意的。设备指纹是截断的 SHA-256 派生值,用作设备唯一性判据,因此它是单向值而不是被加密值。客户端本地的 ~/.magicfrog/credentials/device.json 保存进程必须出示的设备 token,受 0600 文件权限保护;只存哈希的规则作用于服务端持久化行。审计记录与日志永不携带明文凭据、配对码或 provider 密钥,只记录截断后的非敏感参数投影。
Docker Compose(可选路径)
Compose 把同一个服务端跑成容器:单个 magicfrog 服务,背后是 Docker 命名卷上的 SQLite 数据库。镜像来自 GHCR 发布面 ghcr.io/gsc-org/magicfrog:<version>——GHCR 上的镜像发布目前不在现行发布面内,将另行公告;在此之前请使用单文件服务端。持久化与备份是卷操作:用 docker volume inspect magicfrog_magicfrog-data 找到宿主机上的位置。docker compose down 保留卷;down -v 删除卷。
Compose 首启导出与单文件相同的变量外加一个版本标签,然后启动栈:
export MAGICFROG_VERSION=<published-version> # a real GHCR release tag
export MAGICFROG_PASSWORD='<server-api-password>'
export MAGICFROG_ADMIN_IDENTIFIER='admin@example.com'
export MAGICFROG_ADMIN_PASSWORD='<initial-admin-password>'
export MAGICFROG_FIELD_KEY='<64-hex-chars>'
docker compose up -d
docker compose logs -f magicfrog
五个变量全部必填:Compose 对它们做严格插值,缺任何一个就报错退出,所以这个栈绝不会带着空凭据启动。端口默认 4096;用 MAGICFROG_PORT 覆盖主机端口映射。容器内部,所有可变状态都位于 /data 之下(映射到命名卷)。
下载分发
已发布产物——magicfrog 客户端与单文件 magicfrog-server——由专用 R2 bucket magicfrog-downloads 提供,客户端直连该 bucket 下载——不经 files Worker 代理。该 bucket 与既有 files bucket 相互分离,不与它们共享对象键命名空间:
| 面 | Bucket | 键命名空间 | 读取路径 |
|---|---|---|---|
| Files 服务(既有,不变) | magicfrog-dev-files / magicfrog-production-files |
bin/、releases/、videos/ |
Worker 路由 /files/* |
| 下载分发 | magicfrog-downloads |
downloads/<version>/ |
R2 公读直连 |
一次性设置与发布:
bunx wrangler r2 bucket create magicfrog-downloads
# Enable public read on the bucket (custom domain or r2.dev dev domain) to get the origin:
export MAGICFROG_DOWNLOAD_BASE_URL='https://<public-read-origin>'
MAGICFROG_DOWNLOAD_BASE_URL='https://<public-read-origin>' bun packages/cli/script/publish.ts
发布脚本把对象写在 downloads/<version>/<filename> 之下,并附上指向 <origin>/downloads/<version>/<filename> 的 url。不带 MAGICFROG_DOWNLOAD_BASE_URL 发布会显式失败——它绝不写占位域名,也不回落到 files bucket。不要把 magicfrog-downloads 加进 services/files 的 wrangler.jsonc r2_buckets;那个 Worker 只绑定既有 files bucket。
完整性校验
发布步骤为每个产物文件记录 sha256 与 size,从上传的字节计算得出,与 url 放在一起:
{ "url": "https://<origin>/downloads/<version>/<filename>", "sha256": "<64 hex>", "size": 12345678 }
客户端下载必须重算这两个值并与元数据比对;任何不一致都会直接拒绝安装。没有「照样安装但警告」的降级路径,也没有占位成功。
未签名产物
Windows 与 macOS 产物在没有代码签名的情况下也是合法交付。签名与公证不是发布门:发布管线不检查签名状态,并打印显式的未签名提示。经 Azure Trusted Signing 的 Windows 签名是可选步骤,未配置时跳过并打印原因;macOS 公证依赖注入发布 workflow 的 Apple 凭据,同样可选。SmartScreen 与 Gatekeeper 的「未知开发者」提示是宿主 OS 行为,不是产品故障,验收中也不视为阻断。
客户端发现
桌面或 web 客户端面对自部署启动时,显式解析服务端。未配置服务端时,它呈现未配置状态并提供服务端地址输入框,而不是静默继续。显式输入的服务端地址恒胜过自动发现。
管理控制台(/admin 管理面)需要在 console 构建时把 VITE_ADMIN_CONTROL_PLANE_URL 设为控制面源。变量未设置时,admin 登录门报告缺失配置,而不是把请求瞄准猜出来的源。console 与控制面不同源时,在服务端用 magicfrog serve --cors <origin> 放行 console 的源。
故障排查
| 症状 | 原因 / 处置 |
|---|---|
| 服务端启动失败,报告缺少必需变量 | MAGICFROG_PASSWORD / MAGICFROG_ADMIN_IDENTIFIER / MAGICFROG_ADMIN_PASSWORD / MAGICFROG_FIELD_KEY 之一未设置;见「必需环境变量」 |
首启日志报告 MAGICFROG_ADMIN_IDENTIFIER 与 MAGICFROG_ADMIN_PASSWORD 必须一起注入 |
两个 admin 变量只给了一个;成对注入 |
| 启动在字段加密密钥上失败 | MAGICFROG_FIELD_KEY 缺失、为空或非法——用 openssl rand -hex 32 生成一个;见「字段加密密钥」 |
业务端点返回 403 password_change_required |
强制改密尚未完成;见「强制改密」 |
docker compose up 报 set MAGICFROG_VERSION … |
某个必需变量未导出;见「Docker Compose」 |
发布时报 MAGICFROG_DOWNLOAD_BASE_URL is required |
下载源未配置;见「下载分发」 |
| 客户端下载返回 404 | 对象键必须是 downloads/<version>/<filename>,且 bucket 已开公读 |
POST /v1/admin/computers 返回 503,消息涉及 iptables 与 Permission denied |
宿主以非 root 用户运行资源池,per-instance egress 策略装不上。要么以 root 运行资源池,要么为 iptables 配置免密 sudo 并设置 MAGICFROG_POOL_HOST_COMMAND_PREFIX="sudo -n"。请求失败即关闭——不写计算机行也不写审计条目,修复后重试是安全的 |