在 Show HN 上出现的 Pi pod,思路是把「编码智能体」跑在一个一次性的、被限制住手脚的容器里:它能读写你指定的代码目录、能执行命令,但碰不到宿主机的其他文件,也默认连不上公网。下面这份教程,是在自己的服务器上把这套沙箱搭起来、把隔离做扎实、再往里派活的完整流程。
> 说明:Pi pod 的镜像名、CLI 子命令、端口与配置项会随版本变化。下文给出的命令是通用的容器与网络写法,可直接改用的模板,具体字段请以官方文档当前版本和 --help 输出为准。
适用场景
适合已经在服务器上跑模型 API、想让编码智能体帮忙改代码、跑测试、做批量重构,但又不放心把整台机器交给它的人。典型场景有三类:让智能体自动修 CI 报错、在隔离环境里跑不可信的第三方脚本、给团队里多个成员各发一个互不干扰的临时工作区。核心诉求是「给它一个目录和一条任务,跑完就回收,出不了圈」。
环境与前置条件
- 操作系统:Linux(x86_64 或 arm64),内核 5.x 以上,推荐 Ubuntu 22.04 / Debian 12 一类的长期支持发行版。
- 运行时:Docker Engine 与 Docker Compose v2 插件(
docker compose子命令形式)。安装方式以 Docker 官方文档当前版本为准。 - 权限:能执行
docker的普通用户即可,不建议全程用 root 操作;把用户加入docker组。 - 资源建议:单任务沙箱 2 核 CPU、2~4 GB 内存起步,工作区磁盘按仓库大小预留,另外给
/var/lib/docker留出至少 20 GB。并发几个沙箱就按倍数放大。 - 网络:服务器本身能出网(拉镜像、调模型 API),沙箱容器默认不出网。
- 凭据:模型服务的 API Key 或自建推理服务的地址,放在环境变量文件里,不要写进镜像。
分步骤部署
第 1 步:建一个专用目录,放配置和密钥
```bash
sudo mkdir -p /opt/pi-pod/{config,workspaces,secrets}
sudo chown -R "$USER":"$USER" /opt/pi-pod
cd /opt/pi-pod
```
config 放沙箱的配置文件,workspaces 放各个任务的工作目录,secrets 放模型凭据。把三者分开,是为了后面备份和权限收窄都方便。
第 2 步:准备模型凭据文件
```bash
cat > /opt/pi-pod/secrets/model.env <<'EOF'
MODEL_BASE_URL=https://your-endpoint.example.com/v1
MODEL_API_KEY=replace-with-your-key
MODEL_NAME=your-model-name
EOF
chmod 600 /opt/pi-pod/secrets/model.env
```
这个文件只被 compose 读取并注入容器环境变量,不打进镜像,也不会随工作区一起挂载进去。
第 3 步:写一份 docker-compose.yml
下面这份模板把隔离项都打开了:只读根文件系统、丢弃全部 Linux capability、禁止提权、限制进程数、非 root 用户运行。
```yaml
/opt/pi-pod/docker-compose.yml
services:
pi-pod:
image: ${PI_POD_IMAGE:?请在 .env 中设置镜像名}
container_name: pi-pod
restart: unless-stopped
env_file:
- ./secrets/model.env
environment:
- SANDBOX_WORKSPACE=/workspace
- SANDBOX_NETWORK_POLICY=deny # 默认拒绝出网
- HTTP_PROXY=${SANDBOX_PROXY:-}
- HTTPS_PROXY=${SANDBOX_PROXY:-}
volumes:
- ./config:/etc/pi-pod:ro
- ./workspaces:/workspace
tmpfs:
- /tmp:size=512m,mode=1777
read_only: true
user: "1000:1000"
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
pids_limit: 512
mem_limit: 4g
cpus: 2.0
networks:
- sandbox_net
healthcheck:
test: ["CMD", "pi-pod", "healthcheck"]
interval: 30s
timeout: 5s
retries: 3
start_period: 20s
networks:
sandbox_net:
driver: bridge
internal: true # 关键:不给这个网络配默认网关
```
同时建一个 .env:
```bash
cat > /opt/pi-pod/.env <<'EOF'
PI_POD_IMAGE=以官方文档当前版本的镜像名为准
EOF
```
internal: true 让容器只有内部 IP、没有默认路由,沙箱里 curl 外网会直接失败。如果模型需要联网调用,把 SANDBOX_PROXY 指向一个白名单代理,只放通模型域名,比直接给沙箱开公网安全得多。
第 4 步:准备第一个工作区
```bash
mkdir -p /opt/pi-pod/workspaces/demo
cd /opt/pi-pod/workspaces/demo
git clone --depth 1 https://example.com/your/repo.git .
```
注意容器内的用户是 UID 1000,宿主机上工作区目录的属主也要对得上,否则沙箱写文件会报权限错误:
```bash
sudo chown -R 1000:1000 /opt/pi-pod/workspaces/demo
```
第 5 步:启动沙箱
```bash
cd /opt/pi-pod
docker compose pull
docker compose up -d
docker compose ps
```
docker compose ps 里 STATUS 一列从 starting 变成 healthy,就说明沙箱起来了。首次启动会拉镜像,耐心等一会儿。
第 6 步:下发一个任务
沙箱本身只是个执行环境,任务是「扔进去」的。两种常见方式:
方式一,用 CLI 一次性下发(跑完即走):
```bash
docker compose exec -T pi-pod \
pi-pod run \
--workspace /workspace/demo \
--task "修复测试失败:先运行测试,定位失败用例,改代码后重新运行验证" \
--timeout 600
```
方式二,走 HTTP 接口,适合接到自己的流水线里:
```bash
curl -fsS -X POST http://127.0.0.1:8080/tasks \
-H 'Content-Type: application/json' \
-d '{
"workspace": "/workspace/demo",
"task": "把 README 里的安装步骤更新为当前实际的依赖安装方式",
"timeout_seconds": 300
}'
```
接口路径、端口与字段名请以官方文档当前版本为准;pi-pod --help 与 pi-pod run --help 是最快的确认方式。
验证部署是否成功
分四层验证,逐层过一遍:
第一层,容器在跑且健康:
```bash
docker compose ps
docker inspect --format '{{.State.Health.Status}}' pi-pod
```
预期输出 healthy。
第二层,隔离确实生效。检查只读根和网络:
```bash
docker inspect --format '{{.HostConfig.ReadonlyRootfs}}' pi-pod
docker inspect --format '{{.HostConfig.NetworkMode}}' pi-pod
docker compose exec -T pi-pod sh -c 'touch /should-fail' || echo "根文件系统只读,符合预期"
docker compose exec -T pi-pod sh -c 'curl -m 5 -sS https://example.com >/dev/null && echo 能出网 || echo 出网被拦,符合预期'
```
预期是第一条 ReadonlyRootfs 为 true,第二条输出「根文件系统只读,符合预期」,第三条输出「出网被拦,符合预期」。
第三层,工作区可写、任务有产物:
```bash
docker compose exec -T pi-pod sh -c 'echo hello > /workspace/demo/.sandbox-probe'
cat /opt/pi-pod/workspaces/demo/.sandbox-probe
```
宿主机能看到 hello,说明挂载和 UID 都对。
第四层,真的能干活。下发一个最小任务:
```bash
docker compose exec -T pi-pod \
pi-pod run --workspace /workspace/demo --task "在当前目录新建 NOTES.md,写一行今天的日期"
ls -l /opt/pi-pod/workspaces/demo/NOTES.md
```
文件出现在宿主机目录里,且容器日志里能看到任务从开始到结束的完整过程,就算部署成功。
常见报错与解决
报错:permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
原因:当前用户不在 docker 组,没有权限访问 Docker 守护进程。
解决:
```bash
sudo usermod -aG docker "$USER"
newgrp docker # 或退出后重新登录
docker info
```
报错:容器启动后立刻退出,日志里出现 401 Unauthorized 或 MODEL_API_KEY is not set
原因:凭据没注入成功,通常是 env_file 路径写错,或者变量名和沙箱期望的不一致。
解决:
```bash
cd /opt/pi-pod
docker compose config | grep -A3 environment # 确认变量被正确解析
docker compose logs --tail 100 pi-pod
```
确认变量名与官方文档一致后,docker compose up -d --force-recreate。
报错:沙箱内 git clone 或调用模型接口超时 connection timed out
原因:internal: true 的网络没有出口,这是隔离生效的表现。需要出网的任务必须走代理。
解决:在 .env 里设置 SANDBOX_PROXY=http://宿主内网IP:代理端口,代理侧只放通模型与代码托管域名,然后 docker compose up -d 重建容器。
报错:沙箱写工作区时 Permission denied
原因:容器内是 UID 1000,宿主机工作区目录属主不是它。
解决:
```bash
sudo chown -R 1000:1000 /opt/pi-pod/workspaces/<任务目录>
```
报错:任务中途被 Killed,或 docker compose up 报磁盘不足
原因:内存或磁盘触顶,也可能是进程数被 pids_limit 卡住。
解决:
```bash
docker system df # 看镜像和卷占了多少
docker system prune -f # 清理停止的容器、悬空镜像
```
再按需调大 compose 里的 mem_limit、cpus、pids_limit。
后续维护
备份。 需要备份的是三样:/opt/pi-pod/config(沙箱配置)、/opt/pi-pod/workspaces(任务产物)、以及你自己的 .env 与凭据文件。镜像是可以重新拉的,不必备份。工作区本质是 Git 仓库,产物尽量通过提交和推送落到远端,宿主机上只留短期副本。
升级。 镜像不要用 latest 之类的浮动标签,改用带版本或 digest 的固定标签,升级前先在 /opt/pi-pod/staging 里用同一份 compose 起一个副本试跑一个任务,确认没问题再切生产。升级流程固定为:docker compose pull → docker compose up -d → 跑一遍上面的四层验证。
日志。 容器日志默认走 json-file 驱动,长期跑会吃满磁盘,给它加上轮转:
```yaml
logging:
driver: json-file
options:
max-size: "20m"
max-file: "5"
```
排查问题时用 docker compose logs --since 30m pi-pod 看时间窗内的日志。每个任务建议在日志里打出任务 ID、工作区路径、耗时和退出码,方便事后对齐。
监控。 healthcheck 已经在 compose 里配好了,再把它接到监控系统上:用 node-exporter 看宿主机的 CPU、内存、磁盘,用 cAdvisor 看容器级资源,用 Prometheus + Alertmanager 对「容器不健康」「磁盘使用率超过 80%」「任务连续失败」三类情况告警。沙箱的价值在于「圈得住」,而圈得住的前提是你知道它在里面干了什么。
收尾习惯。 每个任务用一个独立的工作区目录,跑完把目录归档或删掉,不要多个任务共用同一个目录;API Key 定期轮换;定期执行一次 docker system prune,并复核 cap_drop、read_only、internal 这三个隔离开关有没有被谁改掉。
