踩坑一周:把 AI Agent 塞进 K8s 沙箱的完整指南
OpenSandbox + Hermes Agent,从镜像构建到冷启动优化的全流程踩坑记录。
前言
最近在搞一件事:把 AI Agent 安全地跑在 K8s 沙箱里 。选了两个开源项目拼起来:OpenSandbox(阿里开源的沙箱服务,Apache 2.0)+ Hermes Agent(Nous Research 的 AI Agent 运行时,MIT)。
听起来就是"部署个镜像,启动个容器"的事,实际上从镜像构建到 K8s 部署到冷启动优化,每一步都有坑。这篇文章记录完整流程和踩坑点,希望能帮到同样在搞这套组合的人。
环境说明:本文基于 OpenSandbox + Hermes Agent 的部署实践。核心踩坑点(镜像分层、架构校验、节点预热、imagePullPolicy)与具体 Agent 实现无关,换任何 Agent 镜像都适用。
为什么要套两层沙箱?
看到这个架构,你的第一个问题肯定是:Hermes 自己就支持 Docker 后端,为什么要再套一层 OpenSandbox?
好问题。两层沙箱管的不是同一件事:
| Hermes 自带沙箱 | OpenSandbox | |
|---|---|---|
| 隔离层级 | 工具执行级(跑 shell 命令的容器) | 基础设施级(整个 Agent 进程的容器) |
| 管什么 | Agent 执行 ls、pip install 等命令时隔离 |
Agent 整个运行环境的生命周期和资源 |
| 生命周期 | 命令级,执行完就释放 | 沙箱级,创建→暂停→恢复→销毁,API 驱动 |
| 多租户 | ❌ | ✅ 每个沙箱独立资源配额 |
| 资源控制 | 进程级 | K8s 级(CPU/内存/GPU limit) |
| 网络策略 | ❌ | ✅ 出站白名单/黑名单 |
| 预热池 | ❌ | ✅ Pool 预热消除冷启动 |
一句话:Hermes 的沙箱管的是"Agent 手脚怎么动",OpenSandbox 管的是"Agent 在哪个房间里动"。
什么时候需要 OpenSandbox + Hermes 这个组合?
- 你需要 API 驱动 创建/销毁沙箱,而不是手动
hermes up启动 Agent - 你需要 多租户隔离,不同用户的 Agent 跑在不同沙箱里,资源互不干扰
- 你需要 基础设施级资源控制(K8s 调度、CPU/内存 limit、网络策略)
- 你需要 沙箱预热池,按需秒级创建 Agent 实例
如果只是个人跑一个 Agent 处理日常任务,Hermes 自带的 Docker 后端就够了,不需要 OpenSandbox。这篇文章面向的是平台级部署场景------你的服务需要按需创建多个隔离的 Agent 实例,交给不同用户使用。
最终架构
先看我们要达成的效果:
arduino
创建沙箱请求(API)
↓
OpenSandbox Server(K8s 内)
↓ 调度 Pod
Hermes Server 容器
├── FastAPI 服务(port 8765)
├── 接收 skill 注册/触发
└── 调用 LLM 执行 Agent 任务
核心目标:通过 OpenSandbox API 创建沙箱,沙箱内跑 Agent,外部通过 HTTP 调用 Agent 能力。
一、镜像构建:三层分层策略
这是第一个大坑。Agent 镜像如果直接一个 Dockerfile 撸到底,每次改一行代码都要重打 800MB+ 的镜像,等到你怀疑人生。
三层构建方案
bash
Dockerfile.base → agent-base(依赖层,~818MB)
│ 系统库 + npm install + Playwright + uv/gosu
│ + Python venv + 预装依赖
↓ 只在依赖变更时重打
Dockerfile → agent-app(代码层)
│ COPY 源码 + npm run build + uv editable link 刷新
↓ 改代码时只需重打此层(秒级~分钟级)
sandbox/Dockerfile → agent-server(启动壳)
FROM agent-app + chmod start.sh + 覆盖 ENTRYPOINT
几秒即可重打
什么时候重打哪一层
| 变更内容 | 重打 base | 重打 agent | 重打 server |
|---|---|---|---|
| Agent 业务代码 | - | ✅ | ✅(几秒) |
| package.json / npm 依赖 | ✅ | ✅ | ✅(几秒) |
| Python 依赖(pyproject.toml extras) | ✅ | ✅ | ✅(几秒) |
| 系统库(apt 包) | ✅ | ✅ | ✅(几秒) |
| sandbox/server.py / start.sh | - | ✅ | ✅(几秒) |
注意 :下文用 agent-base、agent-app、agent-server 作为示例镜像名,实际使用时替换为你自己的命名。
关键理解 :Python 依赖预装在 base 层,代码层只做 uv pip install --no-deps -e "." 刷新 editable link。这把代码层重打时间从几分钟降到几秒,主要耗时只剩 npm run build。
构建命令
⚠️ 踩坑 1:架构问题
如果你的开发机是 Mac M 系列芯片(arm64),而服务器是 linux/amd64,构建时必须 指定 --platform linux/amd64,否则服务器上会报 exec format error(后面详说)。
bash
# 1. 构建 base(首次或依赖变更时)
docker buildx build --platform linux/amd64 \
--network host --load \
-t agent-base:latest \
-f Dockerfile.base .
# 校验架构!这步不能省
docker image inspect agent-base:latest --format 'ARCH={{.Architecture}} OS={{.Os}}'
# 期望输出:ARCH=amd64 OS=linux
# 2. 构建 agent(改代码后只需此步)
docker buildx build --platform linux/amd64 \
--network host --load \
-t agent-app:latest .
# 再次校验
docker image inspect agent-app:latest --format 'ARCH={{.Architecture}} OS={{.Os}}'
# 3. 构建 server(几秒)
docker buildx build --platform linux/amd64 \
--network host --load \
-t agent-server:v1.0.0 \
-f sandboxes/hermes/Dockerfile .
docker image inspect agent-server:v1.0.0 \
--format 'ARCH={{.Architecture}} OS={{.Os}} ID={{.Id}}'
💡 提示 :Dockerfile.base 默认从 ghcr.io/astral-sh/uv 取 uv 二进制。如果网络不通(症状:EOF 或 SSL_CONNECT_ERROR),需要分情况处理:
如果已有旧 base 镜像(非首次构建):把 FROM 改为从旧 base 取:
vbnet
# 将这行:
FROM ghcr.io/astral-sh/uv:0.11.6-python3.13-trixie@sha256:... AS uv_source
# 改为:
FROM agent-base:latest AS uv_source
本地旧 base 已包含 uv 二进制,无需联网。
如果是首次构建,没有旧 base:三种方案:
| 方案 | 操作 | 适用场景 |
|---|---|---|
| 代理/VPN | 临时开代理构建 base,构建完关掉 | 最简单,有代理条件首选 |
| 手动放 uv 二进制 | 在有网环境下载 uv 二进制,COPY 进镜像替代 ghcr.io 拉取 |
无代理但有办法传文件 |
| 离线导入 | 在有网机器上先构建 base → docker save 导出 tar → 传到目标机器 docker load |
完全离线环境 |
手动放 uv 二进制的示例(修改 Dockerfile.base):
bash
# 替代从 ghcr.io 拉取,直接 COPY 本地预下载的 uv
COPY --chmod=755 uv /usr/local/bin/uv
uv 二进制可以从 uv 官方 GitHub Release 下载对应平台的版本。
建议 :首次构建 base 后,务必保留 agent-base 镜像不要删。后续依赖变更重打 base 时,就可以用"从旧 base 取"的方案绕过 ghcr.io 了。
导出与导入
由于服务器可能无法直连镜像仓库,需要离线导入:
ruby
# 导出 tar
docker save -o agent-server_v1.0.0.tar agent-server:v1.0.0
# 上传服务器
scp agent-server_v1.0.0.tar user@server:/tmp/
# 服务器导入
docker load -i /tmp/agent-server_v1.0.0.tar
# 验证架构
docker image inspect agent-server:v1.0.0 --format '{{.Architecture}}/{{.Os}}'
二、OpenSandbox 创建沙箱
镜像就绪后,通过 OpenSandbox API 创建沙箱:
json
{
"image": {
"uri": "agent-server:v1.0.0"
},
"timeout": 3600,
"resourceLimits": {
"cpu": "2",
"memory": "4Gi"
},
"entrypoint": ["/opt/hermes/sandbox/start.sh"],
"env": {
"ANTHROPIC_API_KEY": "sk-ant-your-key-here"
},
"metadata": {
"owner": "your-team",
"purpose": "hermes-agent"
}
}
⚠️ 踩坑 2:资源配额
OpenSandbox 的 resourceLimits 目前是 requests = limits 的静态配置,没法单独设 requests。给少了 Agent 跑不动------尤其是加载大模型时内存需求会飙升,建议起步 2C4G,跑大模型直接上 8G+。
三、冷启动优化:这才是重头戏
问题有多严重?
agent-server 镜像约 800MB ,跨网络拉取需要 70s ~ 2min。如果每次创建沙箱都要拉镜像,用户体验就是"点一下等两分钟",完全不可用。
解决方案:节点镜像预热
核心思路:把镜像预先缓存到所有 K8s 节点本地,配合 imagePullPolicy: IfNotPresent ,沙箱启动时直接用本地缓存。
Step 1:部署 DaemonSet 预拉镜像
yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: agent-image-puller
namespace: your-namespace
spec:
selector:
matchLabels:
app: agent-image-puller
template:
metadata:
labels:
app: agent-image-puller
spec:
initContainers:
- name: pull-agent
image: agent-server:v1.0.0
command: ["sh", "-c", "echo agent image pulled ok"]
imagePullPolicy: IfNotPresent
containers:
- name: pause
image: registry.k8s.io/pause:3.9
imagePullSecrets:
- name: your-pull-secret
💡 原理:DaemonSet 会在每个节点上跑一个 Pod,Pod 的 initContainer 会拉取 Agent 镜像。拉完后镜像就缓存在节点本地了。后面的 pause 容器什么都不干,只是让 Pod 保持运行状态。
⚠️ 踩坑 3: registry.k8s.io/pause 可能拉不到
在内网环境,registry.k8s.io/pause:3.9 可能无法访问,Pod 会一直 ImagePullBackOff。但不影响 Agent 镜像的预拉------initContainer 先执行,镜像拉完后缓存即生效。可以忽略这个报错。
Step 2:确认各节点缓存状态
arduino
for pod in $(kubectl get pods -n your-namespace -l app=agent-image-puller \
-o jsonpath='{.items[*].metadata.name}'); do
echo "=== $pod ==="
kubectl describe pod $pod -n your-namespace \
| grep -E "already present|Successfully pulled|Failed to pull|Pulling image" \
| grep agent
done
每个节点看到以下任意一条即表示缓存完成:
Container image "..." already present on machine--- 节点之前就有缓存Successfully pulled image "..."--- 本次拉取完成
Step 3:确认 imagePullPolicy
⚠️ 踩坑 4:这步最容易被忽略!
即使节点已有缓存,如果沙箱 Pod 的 imagePullPolicy 是 Always,K8s 还是会每次跨网验证镜像(虽然不会重新拉取,但验证本身也要 10~30s)。
swift
# 查看当前沙箱 Pod 的策略
kubectl get pod -n your-namespace -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].imagePullPolicy}{"\n"}{end}' | grep -v opensandbox
如果显示 Always,需要在 OpenSandbox 配置或 CRD 模板中改为 IfNotPresent。
Step 4:清理 DaemonSet
缓存完成后删除 DaemonSet(目的已达到,不需要保留):
arduino
kubectl delete daemonset agent-image-puller -n your-namespace
效果对比
| 场景 | 改造前 | 改造后 |
|---|---|---|
| 节点有缓存 | 每次跨网验证(Always),10~30s |
直接用本地缓存,秒级 |
| 节点无缓存 | 跨网拉取,70s~2min | 首次拉取后缓存,后续秒级 |
四、镜像版本升级流程
每次升级镜像版本,需要重新预热所有节点:
- 修改镜像预热 DaemonSet 中的镜像 tag(如
v1.0.0→v1.1.0) - 重新 apply DaemonSet
- 等所有节点缓存完成
- 删除 DaemonSet
- (可选)重启 OpenSandbox server 使其感知新缓存
五、踩坑总结
坑 1:exec format error
现象 :容器启动报 exec /bin/sh: exec format error
原因:镜像架构不匹配。Mac M 系列默认构建 arm64,服务器是 amd64。
解决 :构建时必须带 --platform linux/amd64,构建后用 docker image inspect 校验架构。
坑 2:Docker 权限不足
现象 :permission denied while trying to connect to the Docker daemon socket
解决 :临时用 sudo,长期把用户加入 docker 组:sudo usermod -aG docker <user> 后重新登录。
坑 3:启动脚本路径错误
现象 :can't open file '/opt/agent-server/skill_server.py': No such file or directory
原因 :镜像内 start.sh 是旧版本,路径指向已废弃的位置,现行路径是 /opt/hermes/sandbox/server.py。
解决:重打 agent-app + agent-server,确保 start.sh 版本一致。
坑 4:ghcr.io 不可达
现象 :failed to do request: Head "https://ghcr.io/...": EOF
解决:将 Dockerfile.base 中 uv 的 FROM 改为从旧 base 取,绕过 ghcr.io。首次没有旧 base 的解决方案见第一章 ghcr.io 部分。
坑 5:冷启动慢但不知道慢在哪
排查方法:用 OpenSandbox 诊断接口看分阶段耗时:
xml
curl "http://<opensandbox-server>/v1/sandboxes/<sandbox-id>/diagnostics/summary?tail=200&event_limit=50"
或 K8s 侧直接看日志:
ini
kubectl logs <pod-name> -n your-namespace -c sandbox --tail=200
六、沙箱内 Python 依赖管理
这是个容易忽略但很实际的问题。
问题
沙箱镜像基于 debian:13.4 minimal,而 Debian 13 启用了 PEP 668 ,直接 pip install 会报 externally-managed-environment 错误。Agent 自己的 Python 环境在 /opt/hermes/.venv/ 是独立的 venv,但沙箱用户脚本需要系统 Python。
解决方案
| 方案 | 命令 | 适用场景 |
|---|---|---|
| apt 预装 | apt-get install python3-requests python3-httpx |
✅ 首选,稳定且不冲突 |
| 临时安装 | pip3 install --break-system-packages <pkg> |
临时用,每次新沙箱要重装 |
| 业务自带 venv | python3 -m venv /tmp/myvenv && /tmp/myvenv/bin/pip install xxx |
隔离最干净 |
pip 镜像源(国内加速):
| 源 | URL |
|---|---|
| 清华源 | https://pypi.tuna.tsinghua.edu.cn/simple |
| 阿里源 | https://mirrors.aliyun.com/pypi/simple |
七、如果你也要搞这套,我的建议
- 镜像分层一定要做,不分层每次改代码重打 800MB 会疯
- 架构校验不能省 ,
docker image inspect看一眼只要 2 秒,排查exec format error要 2 小时 - 节点预热是必选项,800MB 镜像不预热,冷启动 70s+ 用户接受不了
- imagePullPolicy 一定要改 IfNotPresent,缓存了还每次跨网验证等于白缓存
- 先跑通再优化,别一上来就搞预热,先把"能跑"验证了
- 首次构建 base 后保留镜像别删,后续重打 base 就能用"从旧 base 取"绕过 ghcr.io
参考链接
- OpenSandbox GitHub --- 阿里开源沙箱服务,Apache 2.0
- Hermes Agent GitHub --- Nous Research 开源 AI Agent 运行时,MIT
- uv 官方 Release --- ghcr.io 不可达时的 uv 二进制下载
写在最后:这套组合(沙箱 + AI Agent)目前在国内实践的人还不多,网上的资料基本是各自独立的部署文档,拼在一起用的踩坑记录几乎为零。如果你也在搞类似的事,欢迎交流。
Kubernetes Docker AI Agent Hermes 云原生 DevOps