Secure Execution: Docker Sandbox
Knowledge
An agent that can execute code, write files, or run shell commands is powerful -- and dangerous. A misconfigured agent can delete files, access sensitive data, or destabilize the host system. The solution: Every tool execution happens in an isolated Docker sandbox.
Why Sandboxing Is Mandatory
Without a sandbox, an agent has the same permissions as the process running it. This means:
- File system access: The agent can read and write any file the process can reach
- Network access: The agent can send arbitrary HTTP requests -- including to internal services
- Process control: The agent can start or kill other processes
- Resource consumption: An infinite loop or memory explosion affects the host system
In production, this is unacceptable. Docker containers provide a controlled environment with defined boundaries.
Container Architecture for Agents
┌─────────────────────────────────────────────┐
│ Host System │
│ ┌───────────────────────────────────────┐ │
│ │ Agent Orchestrator │ │
│ │ (Planner + Reviewer run here) │ │
│ └───────────┬───────────────────────────┘ │
│ │ Docker API │
│ ┌───────────▼───────────────────────────┐ │
│ │ Sandbox Container (ephemeral) │ │
│ │ - Executor tools run here │ │
│ │ - No network (optional) │ │
│ │ - Limited memory (512MB) │ │
│ │ - CPU limit (1 core) │ │
│ │ - Timeout: 30 seconds │ │
│ │ - Read-only filesystem (+ /tmp) │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
Understanding
Dockerfile for the Agent Sandbox
# Minimal image for agent tool execution
FROM python:3.12-slim
# Security basics: No root user
RUN groupadd -r sandbox && useradd -r -g sandbox sandbox
# Only essential packages
RUN pip install --no-cache-dir \
requests==2.32.0 \
beautifulsoup4==4.12.3
# Working directory
WORKDIR /workspace
# No root
USER sandbox
# Default timeout: 30 seconds
ENV TIMEOUT=30
ENTRYPOINT ["python", "-c"]
Tool Execution in the Container
import docker
import json
from dataclasses import dataclass
@dataclass
class SandboxConfig:
image: str = "agent-sandbox:latest"
memory_limit: str = "512m"
nano_cpus: int = 1_000_000_000 # 1 CPU core
timeout: int = 30
network_disabled: bool = True
read_only: bool = True
class DockerSandbox:
def __init__(self, config: SandboxConfig | None = None):
self.config = config or SandboxConfig()
self.client = docker.from_env()
def execute(self, code: str) -> dict:
"""Executes code securely in a container."""
try:
container = self.client.containers.run(
image=self.config.image,
command=code,
mem_limit=self.config.memory_limit,
nano_cpus=self.config.nano_cpus,
network_disabled=self.config.network_disabled,
read_only=self.config.read_only,
tmpfs={"/tmp": "size=64m"},
detach=True,
remove=False
)
# Wait for result with timeout
result = container.wait(timeout=self.config.timeout)
logs = container.logs().decode("utf-8")
exit_code = result["StatusCode"]
container.remove()
return {
"success": exit_code == 0,
"output": logs,
"exit_code": exit_code
}
except docker.errors.ContainerError as e:
return {"success": False, "output": str(e), "exit_code": 1}
except Exception as e:
return {"success": False, "output": f"Sandbox error: {e}", "exit_code": -1}
# Usage in the multi-agent system
sandbox = DockerSandbox()
result = sandbox.execute(
"print('Hello from the sandbox!'); import os; print(os.getcwd())"
)
TypeScript Variant with Dockerode
import Docker from "dockerode";
interface SandboxResult {
success: boolean;
output: string;
exitCode: number;
}
class DockerSandbox {
private docker: Docker;
constructor(private image = "agent-sandbox:latest") {
this.docker = new Docker();
}
async execute(code: string): Promise<SandboxResult> {
const container = await this.docker.createContainer({
Image: this.image,
Cmd: [code],
HostConfig: {
Memory: 512 * 1024 * 1024, // 512MB
CpuCount: 1,
NetworkMode: "none",
ReadonlyRootfs: true,
Tmpfs: { "/tmp": "size=64m" },
},
});
await container.start();
const result = await container.wait();
const logs = await container.logs({ stdout: true, stderr: true });
await container.remove();
return {
success: result.StatusCode === 0,
output: logs.toString(),
exitCode: result.StatusCode,
};
}
}
Resource Limits: The Key Parameters
| Parameter | Recommended Value | Why? |
|---|---|---|
| Memory | 512MB | Sufficient for most tool tasks, prevents memory explosions |
| CPU | 1 Core | Prevents a container from maxing out the host system |
| Timeout | 30 seconds | Catches infinite loops -- most tools need < 5s |
| Network | Disabled | Prevents data exfiltration and unwanted API calls |
| Filesystem | Read-only + /tmp | Agent can write temporary files but can't change anything persistent |
| PID Limit | 50 | Prevents fork bombs |
Apply
Your agent needs to call an external API (e.g., GitHub) as a tool. How do you solve this in a sandbox with disabled networking?
Reflect
Sandboxing isn't paranoid -- it's professional. Every productive agent that executes code or interacts with external systems needs defined boundaries. Docker containers are the most pragmatic path: They're established, well-documented, and integrate into any CI/CD pipeline. The setup effort pays for itself with the first prevented incident.