Pi Coding Agent 这类工具跑起来之后,我第一个纠结的问题不是模型选哪个、Prompt 怎么写,而是:它生成的代码到底该在哪里执行。
这不是小题大做。我的 Pi Coding Agent 常驻在一块小主机上,每天自动帮我拆需求、写补丁、跑测试,听着省心,可一旦让它直接在主系统里执行任意代码,等于把一个陌生程序员请进了服务器机房,没拴绳。它可能只是犯个小错,把生产数据目录当成测试目录清空;也可能被恶意 prompt 诱导,悄悄读取环境变量里的密钥并发到外部接口,这两种场面我都不想体验第二次。
Docker Sandbox 就是给这类场景准备的:把 Agent 的代码执行环境关进一个可随时丢弃的容器里,CPU、内存、磁盘、网络权限全部可管可控,跑坏了就删掉重建,宿主机始终干干净净。这套方案成本低、见效快,一个树莓派级别的设备都能流畅运行,非常契合 Pi Coding Agent 这种需要频繁试错的常驻任务。
下面把我实际搭环境的过程完整复盘一遍:为什么选 Docker 而不是虚拟机、怎么装 Docker 才不踩坑、沙箱镜像怎么写、资源限制和网络隔离怎么配、日常跑起来又遇到哪些坑。里面每一段都是我真跑过、真翻过车之后沉淀下来的东西,可以直接照抄。
1. 为什么 Pi Coding Agent 需要沙箱:隔离不是可选项
1.1 让 AI 自主执行代码的风险到底有多大
先别急着装 Docker,先把风险摆清楚。Pi Coding Agent 的工作流通常是:接收任务、生成代码、执行、看报错、改代码、再执行。问题就出在"执行"这一步。
代码本身没有善恶,但执行上下文决定后果。在宿主机上直接跑,Agent 生成的代码继承的是你的用户权限,理论上能读你的 SSH 私钥、改你的配置文件、往任意目录写文件、向外网发请求。一次正常的测试中,可能只是安装某个依赖时污染了全局 Python 环境;一次异常对话中,模型可能因为 prompt 注入输出清空目录的指令;一次纯粹的环境配置失误,递归删除命令跑错目录,几秒钟就能让数据归零。
我给自己定了一条铁律:凡是 Agent 生成的代码,一律不进宿主机执行环境。这不是不信任模型,而是不信任"执行环境"这么高的权限被自动操作。把 Agent 当成一个实习生——可以放手干活,但不能给它 root 密码。
1.2 Docker 沙箱的隔离原理:命名空间与控制组
Docker 容器能实现隔离,靠的是 Linux 内核的两个机制:Namespaces 和 Cgroups。
Namespaces 负责让容器里的进程"看起来"拥有独立的世界:独立的进程树、独立的网络栈、独立的文件系统挂载点、独立的用户 ID 映射。容器里看到的 PID 1,在宿主机上可能只是几万号进程里的某一个;容器里绑定的 8080 端口,如果不做映射,宿主机根本感知不到。
Cgroups 负责"分赃":限制这个进程组能用多少 CPU、多少内存、多少磁盘 IO。Agent 里写了个死循环 bug,在裸机上会让整台机器风扇狂转、其他服务卡死;在容器里,它最多把分配给自己的那 0.5 个核吃满,之后被内核强制限制,其余服务不受影响。
这两套机制合在一起,就是性价比极高的"软件级隔离"。它不像虚拟机那样需要模拟整台硬件,开销极小,启动一次只要几百毫秒,特别适合 Agent 这种需要频繁创建、销毁执行环境的场景。
1.3 方案选型:容器、虚拟机、裸进程怎么取舍
给 Agent 搭执行环境,通常有三条路。
虚拟机(如 VirtualBox、QEMU)隔离最彻底,Guest OS 里就算被搞到内核崩溃,也波及不到宿主机。但代价是重:镜像几个 GB、启动按分钟算、内存占用动辄 2GB 起,在小内存设备上基本玩不转。
裸进程加受限用户(比如建个 nobody 用户来跑)成本最低,但隔离极弱。进程可以借助系统调用摸到很多宿主机资源,网络也是通的,密钥泄漏风险降不下来。
Docker 容器正好居中:隔离强度足以应对绝大多数代码执行风险,镜像几百 MB、启动几百毫秒、内存占用可控,还有一套成熟的编排工具链。对 Pi Coding Agent 这种"频繁执行、快速销毁、资源有限"的场景,容器是明显的最优解。如果跑 Agent 的主机上有 GPU 需要直通,容器也能通过 --gpus 参数挂载显卡,这点虚拟机反而很难舒服地做到。
2. 把 Docker 装好:跨平台的实操与避坑
2.1 先选对 Docker 的形态:Desktop 还是 Engine
Docker 有两个常见形态,很多人一开始就栽在这是个选择上。一个是 Docker Desktop,自带图形界面和托盘管理,适合 Windows 和 macOS 开发机;另一个是 Docker Engine,纯命令行守护进程,适合 Linux 服务器。
Pi Coding Agent 如果是跑在树莓派、N100 小主机这类 Linux 设备上,直接装 Docker Engine 就好,桌面版反而多余;如果 Agent 跑在 Windows 开发机上,那就装 Docker Desktop。建议先想清楚最终部署位置再装,免得后面迁移时又重折腾一遍。
Linux 上安装推荐用官方源,Ubuntu 系注意区分两个包名:apt 里那个叫 docker.io 的通常版本较老,建议用 Docker 官方源里的 docker-ce。装上之后确认服务状态:
sudo systemctl enable --now docker docker version看到 Client 和 Server 两段都输出版本号,说明守护进程已经正常运行。只输出 Client 没有 Server,多半是服务没起来或者当前的 shell 会话太旧,重新登录一次再试。
2.2 Windows 上装 Docker Desktop 的三个前置条件
Windows 用户装 Docker Desktop,翻车率最高的是前置条件没到位。装之前先确认三件事:
- 操作系统是 64 位的 Windows 10/11 专业版及以上版本,家庭版需要先手动启用 Hyper-V 相关功能;
- CPU 虚拟化要在 BIOS/UEFI 里打开,Intel 叫 VT-x,AMD 叫 SVM;
- 最好先把 WSL2 装好,Docker Desktop 默认用 WSL2 作为后端,比老旧的 Hyper-V 模式启动快不少。
安装包下载好之后一路 Next 就行,装完大概率会提示重启。重启后打开 Docker Desktop,看到鲸鱼图标变绿才代表真正起来。如果打开后一直卡在 starting 状态,多半是 WSL2 内核没更新,去 Windows Update 里拉一次补丁基本能解决。
公司电脑被组策略限制 Hyper-V 的情况我也遇到过,这类环境更省事的做法是放弃 Desktop,直接装一个 WSL2 发行版,在 WSL 里部署 Docker Engine,Windows 侧通过命令行访问,效果一样,只是没有图形界面。
2.3 启动失败的经典报错:virtualization support not detected
我在给一台老笔记本装 Docker Desktop 时,踩过"Docker Desktop failed to start because virtualization support wasn't detected"这个报错。字面意思是"没检测到虚拟化支持",但实际原因通常是三种:
- BIOS 里虚拟化被关了,进 BIOS 找 VT-x 或 AMD-V 打开;
- Windows 的虚拟机平台功能没启用,去"启用或关闭 Windows 功能"里勾选"虚拟机平台"和"适用于 Linux 的 Windows 子系统",然后重启;
- 装了带内核隔离的第三方安全软件,Hyper-V 组件被禁用,这种只能暂时退出软件再试。
排查时可以在 PowerShell 里执行 systeminfo,看"Hyper-V 要求"那一段是不是全都显示"是"。如果显示"否",说明虚拟化确实没开,回 BIOS 处理;全显示"是"还报错,那就是安装过程没触发组件配置,卸载重装一次往往就好了。
3. 构建 Pi Coding Agent 专用沙箱镜像:从 Dockerfile 到成品
3.1 基础镜像的选型逻辑
沙箱镜像的核心是"够用就好"。Pi Coding Agent 要执行的是 Python 代码,那就只给它准备 Python 运行时和最小化工具链,其他一律不装。
我选基础镜像时从 python:3.11-slim 入手,而不是 python:3.11。slim 版本基于 Debian 的精简骨架,自带 Python 解释器,体积只有标准版的三分之一左右。标准版带了一堆编译器和文档,沙箱里根本用不上,徒增攻击面。
如果你需要跑 Node.js 或者 Go,思路一样:找官方镜像的 slim 变体。alpine 更小但用的是 musl libc,有些 Python 的二进制扩展包装不上;slim 用 glibc,兼容性更好,所以我优先推荐 slim 系列。
有一点要提前想好:Agent 大概率需要额外装第三方库,比如 requests、pytest。这些依赖应该在构建阶段就装进镜像里,而不是每次启动容器时现场 pip install。现场装一是慢,二是每次启动环境都不一致,排查问题时相当痛苦。
3.2 Dockerfile 逐段拆解
我最终的沙箱 Dockerfile 长这样:
FROM python:3.11-slim # 建一个无权限用户,让 Agent 的代码在这个用户下运行 RUN useradd --create-home --shell /bin/bash agent # 限定工作目录 WORKDIR /workspace # 安装常用依赖,尽量在构建阶段完成 COPY requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir -r /tmp/requirements.txt \ && rm -rf /root/.cache/pip # 切换用户,避免以 root 身份执行 Agent 代码 USER agent # 声明容器入口脚本 COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod +x /usr/local/bin/entrypoint.sh ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]这里有两个关键点。USER agent 是为了让容器内进程不持有 root 权限,即使攻击者拿下了容器,也只是普通用户权限。pip 加 --no-cache-dir 是为了避免缓存文件留在镜像里,既减小体积,又防止敏感信息残留在构建产物中。
再看 entrypoint.sh,它的作用是把 Agent 传进来的命令原样接收并执行:
#!/bin/bash exec "$@"配合启动命令 docker run sandbox:latest python script.py,就能把 python script.py 这个命令送进沙箱执行。exec 关键字很重要,它让脚本进程被替换成目标命令进程,退出信号能正确传递,容器结束时不会残留多余进程。
3.3 构建镜像与瘦身细节
构建命令很简单:
docker build -t picoding-sandbox:latest .构建过程中我踩过一个坑:开发时为了调试,往镜像里装了 vim、curl、git 这些工具,结果镜像体积涨到 800MB。后来意识到沙箱需要暴露的能力越少越好,调试工具应该留在宿主机,不该进镜像。删掉之后体积回落到 250MB 左右。
另一个细节是层数控制。Dockerfile 里每个 RUN 指令会产生一层镜像,层数越多,镜像越大、构建越慢。我习惯把连续的安装操作合并到同一个 RUN 里,用 && 串联,这样不仅层数少,还能避免缓存残留在中间层。
如果想给镜像加上完整性校验,构建完可以记录 diges:
docker images --digests | grep picoding-sandbox把 digest 记下来,后续部署时固定用这个 digest 启动容器,就不会出现"昨天还能跑,今天拉了个新镜像就挂了"这种玄学问题。
4. 资源限制与网络隔离:把沙箱扎紧口袋
4.1 必须配置的三个资源参数
镜像建好了,直接 docker run 也能跑,但那等于给 Agent 发了一张无限额度信用卡。我每次启动沙箱必配三个参数:
- 内存限制 --memory=512m:超过阈值,内核直接 OOM 杀掉容器内进程,不会拖垮宿主机;
- CPU 限制 --cpus=0.5:最多使用半个核心,适合树莓派这类资源紧张的设备;
- 进程数限制 --pids-limit=64:防止 fork 炸弹,Agent 里如果出现并发失控的代码,最多创建 64 个进程就撞上限。
这三个参数组合使用,效果是:Agent 再疯也疯不过 512MB 内存、半颗 CPU、64 个进程。宿主机上其他服务,比如 Pi 本身跑的 Web 面板,始终不受影响。
还有一个容易被忽略的参数是 --read-only,把容器的根文件系统设为只读。Agent 写临时文件应该写到挂载的 tmpfs 或输出目录里,而不是往镜像层写东西。这样可以防止恶意脚本把后门文件藏在容器里,反正重启容器就恢复原样。
4.2 网络访问策略:该给的给,不该给的别给
网络策略我分成两档来看。
默认情况下容器走 bridge 网络,可以正常访问外网。Pi Coding Agent 执行测试代码时通常需要外网,比如 pip install 验证依赖、调用外部 API 做联调,所以大部分场景保留外网是合理的。
但要控制出方向。最狠的一招是 --network none,容器直接没有任何网络接口,适合纯本地计算任务;中间档是 --network bridge,配合防火墙规则只允许访问白名单域名。
实操层面,我出过一次事故:Agent 生成的一段测试代码在沙箱里跑,但它自己拼了内网地址,把宿主机所在局域网里的服务接口扫了一遍。从那之后,我只要不是明确需要联调外部服务,一律给沙箱挂 --network none。让 Agent 专心在本地算,别碰局域网。
如果 Agent 调试时需要本地端口,尽量用回环地址发布:-p 127.0.0.1:8080:8080,只绑本机,别绑 0.0.0.0,避免局域网内其他设备直接访问到沙箱里的服务。
4.3 数据持久化:用最小权限挂载工作目录
Agent 跑完的产物要拿出来,用 volume 解决。但挂载时我会特意给最小权限:
docker run -v /data/agent-workspace:/output:rw picoding-sandbox:latest python build_script.py挂载路径要单独指定,别把整个 /data 挂进去。我第一次图省事,把宿主机家目录挂进容器,结果沙箱里的一次误删操作直接波及宿主机文件,跟隔离初衷背道而驰,当时冷汗都下来了。
更安全一点的做法是挂载只读目录,Agent 只能读输入数据,输出通过 docker cp 拷贝出来。少数情况下 Agent 需要写结果,就单独再挂一个空输出目录,写坏了就删掉重来,反正损失可控。
5. docker compose 编排:一套配置跑完整环境
5.1 docker-compose.yml 参考实现
命令行参数一多就容易忘,复制粘贴也容易错。我用 docker compose 把整份沙箱配置固化成文件,一次写好,到处运行。
下面是我线上在用的 docker-compose.yml 简化版:
services: picoding-sandbox: image: picoding-sandbox:latest container_name: picoding-agent-runner user: agent working_dir: /workspace read_only: true tmpfs: - /tmp mem_limit: 512m cpus: 0.5 pids_limit: 64 security_opt: - no-new-privileges:true cap_drop: - ALL volumes: - /data/agent-input:/workspace/input:ro - /data/agent-output:/workspace/output:rw network_mode: "none" restart: "no" # 长驻任务时取消下一行注释,换成 Agent 服务端命令 # command: ["python", "/workspace/agent_server.py"]每个字段都能跟前面讲的对上:read_only 保护根文件系统,tmpfs 提供临时目录,mem_limit/cpus/pids_limit 限制资源,cap_drop ALL 丢掉所有 Linux 内核能力,network_mode none 关掉网络,volume 只挂最小目录。restart 设成 no,因为沙箱是一次性的,跑完就该销毁,不该被自动拉起。
5.2 启动与验证一套流程
启动沙箱执行一条命令:
docker compose run --rm picoding-sandbox python /workspace/test_case.py--rm 表示容器退出后自动删除,这符合沙箱"用后即焚"的定位。如果想临时进去调试,可以覆盖入口:
docker compose run --rm --entrypoint /bin/bash picoding-sandbox进去之后用 top、df、cat /proc/1/cgroup 这些命令确认资源限制和所属 cgroup 是否符合预期。注意:镜像里我刻意没装 ip 命令,这是有意的,调试时如果临时需要,装完要记得它不在正式镜像里。
5.3 会话与生命周期管理
Agent 跑长任务时,一次执行可能持续几分钟。我用 docker compose run -d 让它后台跑,然后通过 docker ps 找到容器 ID,再用 docker stats 实时观察资源消耗:
docker compose run -d picoding-sandbox python /workspace/long_task.py docker ps docker stats <容器ID>如果发现某个会话把 512MB 内存吃满,反复 OOM,说明这个任务本身有问题,应该去调 Agent 侧的代码逻辑,而不是把沙箱限制放宽。放宽限制一时爽,系统扛不住的时候哭都来不及。
沙箱用完后清场也很关键。我写了两个别名定期清理:
alias docker-clean-ps='docker ps -a --filter "status=exited" -q | xargs docker rm' alias docker-clean-img='docker image prune -f'这套清理逻辑其实是干两件事:删残留容器、回收悬空镜像,避免长期跑 Agent 后磁盘被一层层垃圾堆满。树莓派的 SD 卡空间本来就金贵,这步省不得。
6. 常见问题与排查技巧实录
6.1 Permission denied while trying to connect to the Docker API
这个报错我一开始天天见,尤其是 Linux 上刚装完 Docker 的时候。原因是 docker 命令默认通过 /var/run/docker.sock 访问守护进程,而这个 socket 文件默认只允许 root 用户访问。
最简单的解决办法是把当前用户加入 docker 用户组:
sudo usermod -aG docker $USER newgrp docker注意,加完组要重新登录一次才能生效。有人图省事直接在前面加 sudo 跑所有 docker 命令,我不建议长期这么干——所有命令都带 sudo,等于把自己的权限意识磨没了。如果是在树莓派这类设备上部署,用户组方案足够。
另一种场景是 CI/CD 流水线里报这个错,那通常是执行任务的容器没挂载 docker.sock,或者守护进程监听的是 TCP 端口,配置里连错了地址。检查 DOCKER_HOST 环境变量就能定位。
6.2 容器内网络不通或 DNS 解析失败
沙箱里代码报网络错误,常见两类:一是容器根本不能上网,二是能连通 IP 但域名解析失败。
第一种情况,先确认启动时的网络模式。如果按我前面的配置用的 network_mode none,那容器内外网不通就是符合预期的,得先确认任务到底需不需要外网。如果需要,把 network_mode 改成 bridge 再试。
第二种情况多半是 DNS 问题,容器内 /etc/resolv.conf 指向的 DNS 服务器不可达。临时用 --dns 参数指定一组可用 DNS 能解燃眉之急:
docker run --dns 8.8.8.8 --dns 114.114.114.114 picoding-sandbox:latest python test_net.py更根源的解决办法是检查宿主机本身的 DNS 配置是否正常,因为容器默认继承宿主机的配置。我自己的教训是:自定义 bridge 网络时容易忘掉网关配置,如果手动创建 network 时指定了子网和网关,一定要确认网关地址没写错,否则容器之间互相都连不通。
6.3 镜像拉取慢得离谱:换源与离线分发
镜像下载慢是另一个高频痛点。构建沙箱镜像要拉基础镜像,经常卡很久甚至超时失败。
我习惯在构建前先确认源的问题。Docker 支持配置 registry mirror,在 /etc/docker/daemon.json 里加上镜像加速地址,然后重启 docker:
{ "registry-mirrors": ["https://docker.m.daocloud.io"] }这个配置的原理是让 Docker 优先从镜像源拉取,不用每次都直连 Docker Hub。配置完重启后用 docker info 可以查看 Registry Mirrors 一栏是否生效。
需要坦白的是,公共镜像源稳定性参差不齐,时不时失效。我的备用方案是:基础镜像只拉一次,打好自己的 sandbox 镜像后保存成 tar 包,分发到其他机器时用 docker load 导入,绕开网络问题:
docker save picoding-sandbox:latest | gzip > picoding-sandbox.tar.gz # 目标机器上执行 docker load < picoding-sandbox.tar.gz这套"离线分发"做法在树莓派或者局域网内多台设备部署时特别省事,我有几次就是靠它,在没有稳定外网的环境里半小时完成整套环境迁移。
我把日常排查的经验整理成了一张速查表,贴在工位上随时看:
| 现象 | 根因 | 一句话处置 |
|---|---|---|
| docker: permission denied... | 用户不在 docker 组 | usermod -aG docker $USER 后重新登录 |
| Docker Desktop 报 virtualization support not detected | BIOS 虚拟化关闭或 WSL 组件缺失 | 开 VT-x/SVM,启用虚拟机平台 |
| 沙箱内域名解析失败 | 容器 DNS 不可达 | --dns 指定可用 DNS,或修宿主机 DNS |
| 镜像拉取卡到超时 | 默认源链路慢 | 配置 registry mirror,或离线 save/load |
| 容器能跑但不能联网 | network_mode 设置成 none | 明确是否需要外网,按需改 bridge |
6.4 关于隔离强度:别忘了纵深防御
最后聊一个容易被忽视的点:Docker 沙箱不是绝对保险箱。容器隔离的本质是内核层面的命名空间隔离,但内核漏洞、错误的挂载配置、不合理的特权参数都有可能造成容器逃逸。我见过有人为了让 Agent 能访问 USB 设备,给容器加了 --privileged,那基本等于把沙箱拆了,非常不建议。
我的做法是层层设防:cap_drop ALL 加 no-new-privileges 是底线,绝不给 --privileged;挂载目录最小化且大部分只读;网络默认关闭;沙箱容器用完即删。再配合宿主机侧的备份策略,工作目录定时快照。这样即便某天真发生逃逸,宿主机也还有一道兜底。
我个人的经验是,别把"沙箱"当成一个产品功能,要当成一套纪律。Agent 可以自由发挥,但它的活动范围必须由你画死。配置全部写在 compose 文件里,每一行都注释清楚是干什么的,三个月后再看还能秒懂,这才是真正可维护的隔离方案。
这套 Docker 沙箱方案我在 Pi 设备上跑了小半年,最直观的感受是:折腾的次数少了。以前 Agent 每次跑完代码都要复查一遍系统有没有被改过,现在直接看容器日志就行,跑废了 docker compose run 再来一次,几十秒的事,完全不影响宿主机的其他服务。
如果你也准备给 coding agent 加一道保险,我给三个最小建议:先装好 Docker 并确认权限,再写好带资源限制的 compose 文件,最后把"用完即删"做成肌肉记忆。这三步做完,你的 Agent 就能放手去跑了,而你的系统永远有一条退路。
另外,镜像里的依赖清单记得随时维护。Agent 需要的库只会越来越多,却很少有人主动做瘦身。我每个季度会重新审视一次 Dockerfile,删掉三个月没用过的依赖,沙箱保持精简,跑起来也快。这套东西看着不起眼,真正出事的时候,能替你挡下最难看的那一劫。