DeepAgents 使用 Docker 沙箱执行 Shell 命令:从 0 到生产部署
DeepAgents 使用 Docker 沙箱执行 Shell 命令:从 0 到生产部署
一、为什么需要 Docker 沙箱
DeepAgents 的 execute 工具只有在后端实现 SandboxBackendProtocol 时才会真正执行 Shell 命令。
项目里默认使用的 FilesystemBackend 只负责文件读写,不支持执行命令。所以当 Skill 里出现下面这类指令时,Agent 无法真正运行脚本:
python skills/bank-asset-allocation-report/scripts/generate_report.py ...
直接换成 LocalShellBackend 虽然简单,但它会在宿主机上执行任意命令,存在安全风险,不适合作为 Web API 的生产方案。更稳妥的做法是使用 Docker 沙箱,把命令执行、文件读写都隔离到容器里。
二、DeepAgents 沙箱实现原理
DeepAgents 已经提供了 BaseSandbox,它基于两个核心能力实现了大部分文件操作:
execute():在沙箱里执行 Shell 命令upload_files():把文件传入沙箱download_files():从沙箱取回文件id:沙箱唯一标识
ls、read、write、edit、grep、glob 这些方法都由 BaseSandbox 自动实现,不需要我们重写。
三、Docker 镜像准备
新建 app/docker/Dockerfile:
FROM python:3.12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends grep \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
# 如果沙箱里要运行项目脚本,就安装项目依赖
RUN pip install --no-cache-dir reportlab python-docx markdown openpyxl pypdf
# 容器必须有一个长驻进程,否则启动后会立即退出
CMD ["python3", "-c", "import time; time.sleep(1e9)"]
构建镜像:
docker build -t deepagents-sandbox:latest -f app/docker/Dockerfile .
说明:
python:3.12-slim和项目要求一致。项目pyproject.toml要求>=3.12,<3.13,本地.venv也是 Python 3.12。CMD里的time.sleep(1e9)大约 31.7 年,对开发环境来说等于常驻,而且不占用 CPU。- 如果容器启动后显示
Exited (0),多半是启动命令是裸的python3,Python 解释器没有输入就退出了。
四、实现 DockerSandbox
这里推荐使用 Docker CLI 实现,不依赖 Python 的 docker 包。
这样做有两个好处:
- 避免项目根目录
docker/文件夹和 pip 包docker重名。 - 不需要额外安装 Python SDK,生产环境只需要 Docker CLI。
新建 app/docker/docker_sandbox.py:
from __future__ import annotations
import io
import subprocess
import tarfile
import time
import uuid
from deepagents.backends.protocol import (
ExecuteResponse,
FileDownloadResponse,
FileUploadResponse,
)
from deepagents.backends.sandbox import BaseSandbox
class DockerSandbox(BaseSandbox):
def __init__(
self,
*,
image: str = "python:3.12-slim",
workdir: str = "/workspace",
timeout: int = 120,
volumes: dict | None = None,
) -> None:
self._workdir = workdir
self._timeout = timeout
name = f"deepagents-{uuid.uuid4().hex[:8]}"
cmd = ["docker", "run", "-d", "--name", name, "--workdir", workdir]
for host_path, cfg in (volumes or {}).items():
cmd += ["-v", f"{host_path}:{cfg['bind']}:{cfg.get('mode', 'rw')}"]
cmd += [image, "python3", "-c", "import time; time.sleep(1e9)"]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(f"docker run failed: {proc.stderr}")
self._container_id = proc.stdout.strip()
@property
def id(self) -> str:
return self._container_id[:12]
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
proc = subprocess.run(
[
"docker", "exec", "-w", self._workdir, self._container_id,
"/bin/sh", "-c", command,
],
capture_output=True,
encoding="utf-8",
errors="replace",
timeout=timeout if timeout is not None else self._timeout,
)
return ExecuteResponse(
output=proc.stdout + proc.stderr,
exit_code=proc.returncode,
)
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
responses = []
for path, content in files:
if not path.startswith("/") or ".." in path:
responses.append(FileUploadResponse(path=path, error="invalid_path"))
continue
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tar:
info = tarfile.TarInfo(path.lstrip("/"))
info.size = len(content)
info.mtime = int(time.time())
tar.addfile(info, io.BytesIO(content))
proc = subprocess.run(
["docker", "cp", "-", f"{self._container_id}:/"],
input=buf.getvalue(),
capture_output=True,
)
if proc.returncode == 0:
responses.append(FileUploadResponse(path=path))
else:
responses.append(
FileUploadResponse(
path=path,
error=proc.stderr.decode(errors="replace").strip() or "upload_failed",
)
)
return responses
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
responses = []
for path in paths:
if not path.startswith("/") or ".." in path:
responses.append(FileDownloadResponse(path=path, error="invalid_path"))
continue
proc = subprocess.run(
["docker", "cp", f"{self._container_id}:{path}", "-"],
capture_output=True,
)
if proc.returncode != 0:
err = proc.stderr.decode(errors="replace")
error = "file_not_found" if "not found" in err.lower() or "no such" in err.lower() else err.strip()[:200]
responses.append(FileDownloadResponse(path=path, error=error))
continue
try:
with tarfile.open(fileobj=io.BytesIO(proc.stdout), mode="r:*") as tar:
member = tar.next()
if member is None or member.isdir():
responses.append(FileDownloadResponse(path=path, error="is_directory"))
continue
content = tar.extractfile(member).read()
responses.append(FileDownloadResponse(path=path, content=content))
except Exception as exc:
responses.append(FileDownloadResponse(path=path, error=str(exc)[:200]))
return responses
核心实现说明:
docker run -d创建长驻容器。docker exec在容器里执行命令。docker cp - 容器:/从标准输入接收 tar 包并解压到容器,实现文件上传。docker cp 容器:路径 -把容器文件打包成 tar 输出到标准输出,实现文件下载。
五、接入主 Agent
修改 app/agent/main_agent.py:
from app.docker.docker_sandbox import DockerSandbox
project_root_path = Path(__file__).parents[1].resolve()
(project_root_path / "output").mkdir(parents=True, exist_ok=True)
file_backend = DockerSandbox(
image="deepagents-sandbox:latest",
workdir="/workspace",
timeout=120,
volumes={
str(project_root_path): {"bind": "/workspace", "mode": "ro"},
str(project_root_path / "output"): {"bind": "/workspace/output", "mode": "rw"},
},
)
这里把 app/ 只读挂载到容器的 /workspace,把 output/ 单独以可写方式挂载,这样:
- Agent 能在容器里读取
skills/。 - 生成的文件会写到
/workspace/output/session_xxx。 - 宿主机通过
app/output/session_xxx直接看到结果。
六、验证
先验证基础命令:
docker run -dit --name sandbox-test deepagents-sandbox:latest python3 -c "import time; time.sleep(1e9)"
docker exec sandbox-test python3 --version
再验证 Python 后端:
cd D:/AI Learning/financesearch-agents
.venv\Scripts\python -B -c "
from app.docker.docker_sandbox import DockerSandbox
s = DockerSandbox(
image='deepagents-sandbox:latest',
workdir='/workspace',
volumes={
'D:/AI Learning/financesearch-agents/app': {'bind': '/workspace', 'mode': 'ro'},
'D:/AI Learning/financesearch-agents/app/output': {'bind': '/workspace/output', 'mode': 'rw'},
},
)
print(s.execute('python3 --version'))
s.write('/workspace/output/test.txt', 'hello')
print(s.read('/workspace/output/test.txt'))
"
验证完删除测试容器:
docker rm -f sandbox-test
七、Windows Docker Desktop 常见问题
1. 容器启动后立即变成 Exited (0)
原因通常是启动命令是裸的 python3:
IMAGE COMMAND STATUS
deepagents-sandbox:latest "python3" Exited (0)
Python 解释器启动后没有 stdin 输入,会立即退出。
解决:
- Dockerfile 里加
CMD ["python3", "-c", "import time; time.sleep(1e9)"]。 - 手动运行时使用
docker run -dit ... python3 -c "import time; time.sleep(1e9)"。 - Docker Desktop 需要处于 Linux 容器模式。
2. import docker 指向了项目自己的 docker 目录
如果项目根目录有 docker/ 文件夹,Python 的 import docker 会优先解析到这个目录:
<module 'docker' (namespace) from ['D:\\AI Learning\\financesearch-agents\\docker']>
这不是 pip 包 docker,所以没有 from_env() 方法。
不重命名目录的解决方案就是本文使用的方案:不 import docker,全部改用 Docker CLI。
如果一定要用 Python SDK,则需要把项目根目录的 docker/ 改名,例如 infra/,否则会一直冲突。
3. docker.from_env 需要加载 .env 吗
docker.from_env() 读取的是进程环境变量,不是 .env 文件。
- 本机 Docker Desktop 默认不需要任何环境变量。
- 远程 Docker 时,需要在启动进程前设置
DOCKER_HOST、DOCKER_TLS_VERIFY、DOCKER_CERT_PATH等变量。 - 如果这些变量写在
.env里,需要先调用load_dotenv(),再执行docker.from_env()。
4. Python 版本不一致有影响吗
本地虚拟环境是 Python 3.12.13,项目锁定 Python 3.12,因此 Docker 镜像使用 python:3.12-slim 是正确选择。
真正需要注意的不是版本,而是平台:
- Docker 镜像是 Debian Linux,本地是 Windows。
- 脚本里的路径和 Shell 命令要按照 Linux 书写。
- 如果容器里要运行项目脚本,必须在 Dockerfile 里安装脚本依赖。
八、生产部署建议
1. 每个会话使用独立容器
当前 DockerSandbox 每次实例化都会创建新容器。Web 服务里不要让所有用户共用一个容器,否则文件会互相污染。
建议每个 session_id 对应一个容器,任务结束后:
docker stop <container_id>
docker rm <container_id>
2. 限制挂载权限
生产环境不要直接把整个项目目录以可写方式挂载进容器。
推荐只挂载必要目录:
skills/只读挂载。output/可写挂载。- 数据库配置、密钥、上传文件不要放进容器。
3. 远程 Docker
部署到远程 Docker 时,在服务启动前设置:
DOCKER_HOST=tcp://192.168.1.10:2375
或者:
DOCKER_HOST=ssh://user@192.168.1.10
使用 Docker CLI 方案时,docker run、docker exec、docker cp 都会读取同一个环境变量,不会出现连接不一致的问题。
4. 镜像版本固定
生产环境不要使用 latest,建议固定到具体版本:
docker build -t registry.example.com/deepagents-sandbox:2026.08.10 .
docker push registry.example.com/deepagents-sandbox:2026.08.10
5. 安全控制
- 不要接受不可信用户的任意 Shell 命令。
- 有需要时配合
interrupt_on={"execute": True}做人工审批。 - 容器内不要安装不必要的网络工具和调试工具。
- 定期清理退出状态的历史容器和未使用的镜像。
九、总结
这套方案的核心思路是:
- 用
BaseSandbox继承 DeepAgents 的沙箱接口。 - 只实现
execute、upload_files、download_files、id。 - 使用 Docker CLI 替代 Python SDK,避免项目目录重名问题。
- 通过只读挂载和独立容器实现生产环境的基本隔离。
这样既能让 Agent 真正执行 Skill 里的脚本,又比直接在宿主机上执行命令安全得多。
更多推荐



所有评论(0)