Zum Inhalt springen

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

ParameterRecommended ValueWhy?
Memory512MBSufficient for most tool tasks, prevents memory explosions
CPU1 CorePrevents a container from maxing out the host system
Timeout30 secondsCatches infinite loops -- most tools need < 5s
NetworkDisabledPrevents data exfiltration and unwanted API calls
FilesystemRead-only + /tmpAgent can write temporary files but can't change anything persistent
PID Limit50Prevents 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.