用 Docker Compose 部署 CubePlex
docker compose -f compose.yaml -f compose.tempo.yaml up -d 在单台主机上
部署 CubePlex(backend + frontend + Postgres + Redis + rustfs S3 存储 +
OpenSandbox + Tempo)。它使用和 Kubernetes 部署模式完全相同的容器镜像,只是
编排方式不同。scripts/up.sh 会带上这两个文件,除非 .env 里
TEMPO_ENABLED=false。
1. 前置依赖
| 项 | 要求 |
|---|---|
| 带 Docker 引擎的 Linux 主机 | ≥ 24,带 docker compose v2 |
| 拉取镜像的出网网络 | ghcr.io 和 Docker Hub(或你自己的镜像源) |
| LLM provider 凭证 | 至少一个——见 LLM Provider 配置 |
| 主机开放端口 | frontend 一个(默认 3000),backend 可选一个(默认 8000) |
不需要 Kubernetes,也不需要 Helm。
2. 部署架构
Host
├─ port :3000 → frontend (Next.js) ── 服务端代理 /api/* ──┐
│ │
└─ port :8000 → backend (FastAPI / uvicorn) ◄────────────┘
├─ 依赖 → postgres (named volume)
├─ 依赖 → redis (named volume)
├─ 依赖 → rustfs (S3 存储, named volume)
└─ tempo 在内部 `tracing` 网络(无 host 端口)
启动引导服务(跑完即结束):
backend-migrate alembic upgrade head(backend 启动前置条件)
bucket-init mc mb(幂等创建 rustfs bucket)
所有服务间通信都走 Docker DNS(例如 backend 通过 postgres:5432 访问
Postgres)。主机只暴露 frontend 端口(以及可选的 backend 端口,用于直接
访问 API)。Tempo 不发布到主机;只有 backend 能在内部 tracing 网络访问
:3200 / :4318。
3. 选择镜像
compose 默认使用 GHCR 上的公开预构建发布镜像——无需自己构建:
ghcr.io/cubeplexai/cubeplex-backend:<version>
ghcr.io/cubeplexai/cubeplex-frontend:<version>
从发布页选一个版本 tag——
backend 和 frontend 共用同一个应用版本,例如 v0.9.3——在下一步设为
BACKEND_TAG / FRONTEND_TAG。IMAGE_REGISTRY / IMAGE_REPO 已默认为
ghcr.io / cubeplexai,标准安装无需改动。GHCR 发布镜像是公开的,无需
docker login。
改用自己构建的镜像(私有 registry / 离线 / 打过补丁)
用 Kubernetes 模式的构建脚本——backend 和 frontend 镜像两种方式完全一样:
deploy/kubernetes/scripts/build-and-push.sh
# 推送到 ${REGISTRY}/${REPO}/cubeplex-{backend,frontend}:<YYMMDD>-<branch>-<short-sha>
然后把 .env 里的 IMAGE_REGISTRY、IMAGE_REPO、BACKEND_TAG、
FRONTEND_TAG 指向该构建。
4. 配置(.env + OpenSandbox + 两个 YAML 文件)
四个文件,均已在 .gitignore 中:
| 文件 | 作用 |
|---|---|
.env | 镜像 tag、主机端口映射、基础设施密码。由 docker compose 直接读取,用于 compose.yaml 中的变量替换。 |
config/config.production.local.yaml | 非密钥的运行时配置(模式、公开 URL、cookie 安全性、sandbox 开关)。挂载进 backend。 |
config/config.production.secrets.yaml | 密钥——JWT / CSRF / vault 密钥材料、基础设施密码(必须与 .env 一致)、LLM provider API key。挂载进 backend。 |
config/opensandbox.toml | OpenSandbox server 配置;其 API key 必须和 backend 密钥文件中的 sandbox.api_key 一致。 |
本节只覆盖启动所需的 key。完整的后端配置字段、分层规则和环境变量映射,见 后端配置参考。
4.1 .env
cp .env.example .env
$EDITOR .env
必填:
IMAGE_REGISTRY=ghcr.io
IMAGE_REPO=cubeplexai
BACKEND_TAG=v0.9.3 # 发布页上的一个版本 tag
FRONTEND_TAG=v0.9.3
# openssl rand -hex 16
POSTGRES_PASSWORD=<...>
REDIS_PASSWORD=<...>
RUSTFS_SECRET_KEY=<...>
可选(展示的是默认值):
BACKEND_PORT=8000
FRONTEND_PORT=3000
POSTGRES_USER=cubeplex
POSTGRES_DB=cubeplex
RUSTFS_ACCESS_KEY=cubeplex
OBJECTSTORE_BUCKET=cubeplex
4.2 config.production.local.yaml
cp config/config.production.local.yaml.example config/config.production.local.yaml
$EDITOR config/config.production.local.yaml
| 字段 | 默认值 | 说明 |
|---|---|---|
api.public_url | http://localhost:8000 | 客户端访问 backend 的 URL;如果前面有反向代理,用那个 URL。 |
public_base_url | 同上 | backend 拼接绝对 URL 时使用。 |
frontend_base_url | http://localhost:3000 | backend 重定向浏览器时使用。 |
deployment.mode | single_tenant | single_tenant 在首个用户注册时自动创建组织;multi_tenant 需要显式的组织引导流程。 |
auth.cookie_secure | false | 纯 HTTP 环境下必须保持 false,否则客户端会静默丢弃认证 cookie。 |
sandbox.enabled | true | 必填。它必须保持为 true,且 sandbox.{domain,image,api_key} 都已配置,否则 backend 拒绝启动。 |
database.host、redis.url、objectstore.endpoint 使用 Docker DNS 名称
(postgres、redis、rustfs)——除非你重命名了这些服务,否则不要修改。
4.3 config.production.secrets.yaml
cp config/config.production.secrets.yaml.example config/config.production.secrets.yaml
$EDITOR config/config.production.secrets.yaml
必填:
production:
auth:
jwt_secret: "<openssl rand -hex 32>"
csrf_secret: "<openssl rand -hex 32>"
vault_key: "<Fernet.generate_key()>"
database:
password: "<与 POSTGRES_PASSWORD 相同>"
redis:
url: "redis://:<REDIS_PASSWORD>@redis:6379/0"
objectstore:
access_key: "cubeplex" # 与 RUSTFS_ACCESS_KEY 相同
access_secret: "<RUSTFS_SECRET_KEY>"
jwt_secret、csrf_secret、vault_key 各自的用途和生成方式,见
必需的密钥。
4.4 LLM provider
在 config.production.secrets.yaml 的 production.llm 下配置——完整字段
参考和示例见 LLM Provider 配置。
5. 启动 / 停止 / 日志
# 启动(同时会拉取最新的 tag;除非 TEMPO_ENABLED=false,否则包含 Tempo)
deploy/docker-compose/scripts/up.sh
# 等价的原始 compose(默认安装始终传两个文件)
docker compose -f deploy/docker-compose/compose.yaml \
-f deploy/docker-compose/compose.tempo.yaml up -d
# 查看日志
docker compose -f deploy/docker-compose/compose.yaml \
-f deploy/docker-compose/compose.tempo.yaml logs -f backend
# 停止并移除容器(保留数据卷)
docker compose -f deploy/docker-compose/compose.yaml \
-f deploy/docker-compose/compose.tempo.yaml down
docker compose … down -v 会停止并删除数据卷(Postgres 数据、rustfs
数据、Redis 数据、Tempo 数据)——这是破坏性操作,只在确实要清空部署时使用。
如果缺少 .env、任一 YAML 配置文件或 config/opensandbox.toml,up.sh 会拒绝启动。
升级任务生命周期切换前的数据库
正常启动的 backend-migrate 容器会拒绝直接迁移尚未完成任务生命周期切换的已有数据库。迁移期间必须停止所有旧版后端,避免它们继续创建命令或通知:
# 先设置新版 BACKEND_TAG,再停止此部署中的所有旧 API/worker。
docker compose -f deploy/docker-compose/compose.yaml \
-f deploy/docker-compose/compose.tempo.yaml stop backend frontend
# 同一进程持有数据库迁移锁,回填并验证数据,再应用最终约束。
# 失败后可安全重试同一命令。
docker compose -f deploy/docker-compose/compose.yaml \
-f deploy/docker-compose/compose.tempo.yaml run --rm backend-migrate \
python -m cubeplex.scripts.lifecycle_upgrade --maintenance
# 正常启动会检查切换结果,再启动后端。
deploy/docker-compose/scripts/up.sh
旧版后端仍在运行时,不得执行 --maintenance。如果迁移器报告仍在运行的旧版 run-lifetime 命令或 monitor,需等待它结束或明确停止。对于未记录原 sandbox 实例或执行方进程句柄的活跃命令,也需先处理或停止,再重试。迁移器不会猜测进程已结束,也不会把旧命令关联到替换后的 sandbox。
6. 验证
# 仅健康检查,速度快
deploy/docker-compose/scripts/smoke-test.sh
# 端到端验证,包含一次真实的 LLM 调用
PROMPT="Say the word hello and nothing else." \
deploy/docker-compose/scripts/e2e.sh
e2e.sh 执行流程:
注册 → 登录 → 确定 workspace → 创建对话
→ 发送消息 → SSE 流 → 断言收到 text_delta
全新部署上注册的第一个用户还没有 workspace,脚本会调用 onboarding 创建一个; 之后注册的用户在注册时就已经拿到 workspace,跳过该步。
两个脚本默认针对 localhost;用 HOST、BACKEND_PORT、FRONTEND_PORT
覆盖以针对远程主机运行。
Prometheus 指标
后端在映射的 BACKEND_PORT(默认 8000)上提供 /metrics。
Compose 服务带有用于 Docker 服务发现的 prometheus.io/* 标签;
仅有标签不会配置 Prometheus 抓取任务。同一 Compose 网络中的 Prometheus 容器可使用:
scrape_configs:
- job_name: cubeplex-backend
static_configs:
- targets: ["backend:8000"]
在宿主机上用 curl http://localhost:${BACKEND_PORT:-8000}/metrics 验证。
由于后端端口映射到宿主机,如果指标端点应仅供内部访问,请限制该端口的访问。
指标名称与统计规则见指标列表。
7. 常见故障排查
Backend 反复重启
docker compose -f deploy/docker-compose/compose.yaml logs backend --tail=50
| 现象 | 解决方法 |
|---|---|
CUBEPLEX_AUTH__VAULT_KEY is required | 在 secrets.yaml 中添加 auth.vault_key。 |
connection refused on postgres:5432 | Postgres 还在启动中,通常会自愈——检查 docker compose ps。 |
Provider 'X' not found | default_model: "X/..." 引用的 provider 没有出现在 providers 列表中。 |
HTTP 下登录 cookie 丢失
config.production.local.yaml 的 auth.cookie_secure 必须是 false——
否则浏览器(或 curl)会因为是纯 HTTP 连接而静默丢弃认证 cookie。
Frontend → backend 失败(CORS / 502)
compose.yaml 在 frontend 容器上设置了 CUBEPLEX_API_URL=http://backend:8000,
让 Next.js 通过 Docker 网络在服务端代理 /api/*。如果你改了服务名,也要
同步改这个环境变量。
镜像拉取失败
默认的 GHCR 发布镜像是公开的,无需登录。请确认 BACKEND_TAG /
FRONTEND_TAG 填的是真实存在的发布 tag(见发布页);
出现 manifest unknown / not found 说明该 tag 不存在。
如果你把 IMAGE_REGISTRY 指向了私有镜像源:
docker login ${IMAGE_REGISTRY}
compose 栈会继承 Docker daemon 的登录凭证。
bucket-init 卡住
docker compose -f deploy/docker-compose/compose.yaml logs bucket-init
如果 rustfs 无法访问,检查 rustfs 容器的健康检查——rustfs 在 :9001 上
提供了一个控制台,可以本地访问确认它是否已启动。
8. 必需的 OpenSandbox runtime
CubePlex 在沙箱中执行 agent 的工具调用(bash、文件读写等)。基础 compose 文件会以 Docker runtime 模式部署 alibaba 的 OpenSandbox 生命周期服务器。
服务
基础 compose 栈包含一个服务:
opensandbox-server 镜像: opensandbox/server:latest
挂载: /var/run/docker.sock
读取: /etc/opensandbox/config.toml
端口: 8090
OpenSandbox server 本身是一个普通的 Python/FastAPI 容器。当它收到
POST /sandboxes 请求时,会通过挂载的 socket 与主机 Docker daemon 通信,
拉起兄弟沙箱容器(不是嵌套的)——它们和 CubePlex 运行在同一个 Docker
引擎上,使用独立的 bridge 网络。
opensandbox-server 容器内的任何代码都可以通过 Docker socket 有效地拿到
主机的 root 权限。请将它保留在私有网络内——不要把 8090 端口暴露到公网。
快速开始
cd deploy/docker-compose
# 1. OpenSandbox 配置(已在 .gitignore 中)
cp config/opensandbox.toml.example config/opensandbox.toml
$EDITOR config/opensandbox.toml # 设置 api_key、eip/host_ip、execd_image、egress.image
# 2. backend 密钥 —— sandbox 部分
$EDITOR config/config.production.secrets.yaml
# sandbox:
# domain: "opensandbox-server:8090" # 这个 overlay 里的 Docker DNS 名称
# image: "ghcr.io/cubeplexai/cubeplex-sandbox:v0.9.3"
# api_key: "<与 opensandbox.toml 中 [server].api_key 相同>"
# 3. backend 非密钥配置(示例已启用 sandbox)
$EDITOR config/config.production.local.yaml
# sandbox:
# enabled: true
# secure_access: false # docker runtime 会拒绝 secureAccess=True
# use_server_proxy: false # OpenSandbox v0.1.x 会丢失代理 URL 的端口号,
# # 改用直连主机映射端口
# 4. 启动完整栈
docker compose up -d
需要 operator 自行管理的值(没有模板):
| Key | 位置 | 说明 |
|---|---|---|
opensandbox.toml [server].api_key | config/opensandbox.toml | 必填;必须与 CubePlex 密钥中的 sandbox.api_key 一致。 |
opensandbox.toml [server].eip | 同上 | 返回给 CubePlex 用于端点 URL 的主机/IP;通常是 host.docker.internal。 |
opensandbox.toml [runtime].execd_image | 同上 | 携带 execd 二进制的镜像;主机 Docker 必须能拉取到。 |
opensandbox.toml [egress].image | 同上 | egress sidecar 镜像;因为 CubePlex 总会下发网络策略,所以是必需的。 |
opensandbox.toml [docker].network_mode | 同上 | 对 CubePlex 来说必须是 bridge(见下方兼容性矩阵)。 |
兼容性——Docker 模式 OpenSandbox 下的 CubePlex 功能
Docker runtime 模式相比 Kubernetes 模式的 OpenSandbox 有一些实际限制。
下表基于 opensandbox-server v0.1.14。
secure-access 开关: Docker runtime 会对 secureAccess=True 返回
HTTP 400——secured endpoint 是 Kubernetes ingress-gateway 的特性。
CubePlex 提供了 sandbox.secure_access 配置项,默认 true(与
Kubernetes 模式行为一致);compose 模式的示例配置将其设为 false,这样
CubePlex 会发送 secureAccess: false,Docker runtime 就会接受请求。
设置好这个开关后,聊天 → 沙箱工具调用 → tool_result 全链路可用。
| 功能 | 可以工作 | 不能工作 |
|---|---|---|
| 聊天 → agent 工具调用(在沙箱里执行 bash / 读写文件) | 可以——聊天 → 沙箱 execute → tool_result 全链路可用 | — |
| 网络策略(egress 防火墙) | 可以——egress sidecar 会被创建并以 DNS 模式执行策略,但仅当 [docker].network_mode = "bridge" | 当 network_mode=host 或使用自定义 bridge 网络时会被拒绝 |
| 文件树面板(聊天界面) | 可以 | — |
| 交互式终端面板(聊天界面) | 可以——通过后端的面板反向代理转发 | 需要把 api.public_url 设成用户浏览器可达、且能转发 WebSocket 的后端 URL |
| 实时浏览器面板(Neko,聊天界面) | 可以——与终端走同一条面板代理 | 同样需要 api.public_url |
密钥注入 / env 替换(cbxref_… 占位符) | – | ❌ 不可用。它需要后端的 mTLS 凭证交换监听器 + egress CA/客户端证书,这些由 Kubernetes chart 部署(gen-egress-certs.sh + egress webhook)。compose overlay 不包含这些,sandbox.egress_exchange_host 保持为空,所以注入被禁用。 |
server-proxy 模式(use_server_proxy: true) | – | OpenSandbox v0.1.x 在代理端点 URL 中会丢失端口号。保持 use_server_proxy: false(示例默认值),overlay 通过 extra_hosts 在 backend 和 opensandbox-server 上都配置 host.docker.internal,让它们能访问沙箱容器在主机上映射的 bridge 端口。 |
pvc.claimName 数据卷 | 可以——但被当作 Docker 命名卷处理 | 没有 CSI 特性,不支持 ReadWriteMany |
暂停 / 恢复(POST /sandboxes/{id}/pause 等) | 调用 Docker 的 pause/unpause(cgroup freezer) | 没有落盘的 checkpoint——主机 Docker 重启后暂停状态会丢失。因此 CubePlex 默认 pause_on_idle: false。 |
一句话总结: 在 Docker 模式的 OpenSandbox 下,agent 可以在沙箱里执行
命令、读写文件、浏览文件树,还能打开交互式终端和实时浏览器面板——
这些都通过后端的面板反向代理转发,所以要把 api.public_url 设成浏览器可达、
能转发 WebSocket 的后端 URL。只有密钥注入仍然需要 Kubernetes runtime;那项
请改用 Kubernetes 部署指南。
以下路由在 Docker runtime 上也会返回 501 Not Implemented,尽管它们出现
在 OpenAPI 规范中(CubePlex 目前都没有调用):POST /pools 及相关的
预热 pod 池接口,以及快照相关接口(POST /sandboxes/{id}/snapshots 等)
——两者都是 Kubernetes 专属能力。
验证
docker compose -f compose.yaml ps
# 期望:opensandbox-server Up (healthy)
直接探测 API(在 backend 容器内,通过 Docker DNS):
docker exec cubeplex-backend-1 python -c "
import urllib.request, json
req = urllib.request.Request(
'http://opensandbox-server:8090/sandboxes',
headers={'OPEN-SANDBOX-API-KEY': '<你的 api_key>'},
)
print(urllib.request.urlopen(req, timeout=5).read().decode())
"
# 期望:{"items":[], ...}
端到端验证(CubePlex 聊天 → 沙箱工具调用)需要
config.production.local.yaml 同时满足 sandbox.enabled: true、
sandbox.secure_access: false、sandbox.use_server_proxy: false。发送
类似 ls -la /workspace 的提示词,应该会产生包含沙箱文件系统内容的真实
tool_result。
拆除
docker compose -f compose.yaml down
MITM CA 以及 server 拉起的沙箱容器会保留在主机 Docker 引擎上——它们不属于
本项目的 compose 网络。可以用 docker ps --filter "name=sandbox-" 查看。
Tempo tracing(默认开启)
scripts/up.sh 会启动 Grafana Tempo 3.0.3,除非 .env 里
TEMPO_ENABLED=false。直接跑 docker compose 时必须带上两个文件:
docker compose -f compose.yaml -f compose.tempo.yaml up -d
Tempo 只加入内部 tracing 网络(backend 也加入;frontend、postgres、
OpenSandbox 不加入)。默认文件没有 host 端口。Tempo 的查询 API 没有鉴权——
这张内部网络就是控制面,不是租户隔离。保留 7 天。record_content 为
false。磁盘 JSONL 关闭(CUBEPLEX_TRACING__JSONL__ENABLED=false)。
admin /admin/traces 在 Tempo 被关掉或 query_endpoint 仍为 null 时返回
503。
关闭:不带 compose.tempo.yaml,或给 up.sh 设 TEMPO_ENABLED=false。
这样 Tempo 容器和 CUBEPLEX_TRACING__* 环境变量一起消失,本地 yaml 里的
tracing.enabled: false 不会被覆盖。
自带 Tempo:关掉 overlay,然后在 config.production.local.yaml 里设置
tracing.otlp.endpoint 和 tracing.tempo.query_endpoint。
仅调试用的本机端口——不要当作默认安装:
docker compose \
-f compose.yaml \
-f compose.tempo.yaml \
-f compose.tempo.publish.yaml \
up -d
这会绑定 127.0.0.1:3200。不要在默认 compose 文件的 ports: 列表上做可选
端口插值。
可选:文档解析(docling-serve)
backend 的 read 工具通过调用
docling-serve 实例,
把上传的 PDF / Office 文档转换成 markdown。没有它时,其他文件类型仍然
可用,只是文档解析不可用。可选的 compose.docling.yaml overlay 支持
两种部署形态。
组合部署:同一台主机、同一个 Docker 网络
cd deploy/docker-compose
docker compose \
-f compose.yaml \
-f compose.docling.yaml \
--profile cpu \
up -d
backend 通过 Docker DNS 以 docling-serve-cpu:5001 访问它——不需要手动
打通网络。用 --profile gpu 可以改用 CUDA 镜像
(docling-serve-cu130,需要主机上装有 NVIDIA container runtime)。
独立部署:单独一台主机
把 compose.docling.yaml 复制到一台独立的主机上(比如一台被多个项目
共用的专用 GPU 机器),单独在那里跑起来:
docker compose -f compose.docling.yaml --profile gpu up -d
然后无论 CubePlex 的 backend 跑在哪里,都把它指向那台主机。
配置 backend
不管哪种部署形态,都必须带上 --profile cpu 或 --profile gpu——两个
docling-serve-* 服务都不带 profile 就不会启动(模型下载任务在两种
profile 下都会运行)。把最终的地址写进
config.production.local.yaml:
parsers:
docling_serve:
base_url: "http://docling-serve-cpu:5001" # 组合部署,--profile cpu
# base_url: "http://docling-serve-cu130:5001" # 组合部署,--profile gpu
# base_url: "http://<独立部署主机>:<端口>" # 独立部署
模型下载和镜像源
docling-models 服务首次启动时会把模型集合(layout、table former、OCR、
VLM 模型——共几个 GB)下载到一个具名 volume 里,重启时复用。
docling-serve-cpu 和 docling-serve-cu130 都会等它下载完成后再对外
服务。
如果默认的 GHCR registry 或 HuggingFace 从你的构建主机访问较慢或被墙, 在启动前覆盖:
# 备用镜像 registry(quay.io 镜像,或中国大陆第三方同步——生产使用前请自行核实)
export DOCLING_REGISTRY=quay.io/docling-project
# 或者:export DOCLING_REGISTRY=swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/docling-project
# 模型下载用的 HuggingFace 镜像
export HF_ENDPOINT=https://hf-mirror.com
# HF_TOKEN=hf_xxx # 仅访问受限/私有仓库时需要