Installation¶
Requirements¶
- Python 3.10+
- uv (recommended) or pip
Install with uv (recommended)¶
Install with pip¶
Optional Dependencies¶
Console Toolset¶
For the ready-to-use pydantic-ai toolset:
Docker Sandbox¶
For isolated code execution in Docker containers:
Daytona Sandbox¶
For isolated code execution in Daytona cloud sandboxes (installs daytona-sdk):
Remote Sandbox (client)¶
To use sandboxes that live in another process, so your application never needs
Docker access (installs httpx only):
sandboxd (service)¶
For the service that owns Docker and rents out sandboxes over HTTP. Install this in the sandbox service image, not in your application:
See Remote Sandboxes.
All Dependencies¶
uv add pydantic-ai-backend[console,docker,daytona,remote]
# or
pip install pydantic-ai-backend[console,docker,daytona,remote]
Environment Setup¶
API Key (for console toolset)¶
If using the console toolset with pydantic-ai, set your model provider's API key:
Docker (for DockerSandbox)¶
For using DockerSandbox:
- Install Docker: Get Docker
- Ensure Docker daemon is running
- Pull a base image:
Daytona (for DaytonaSandbox)¶
For using DaytonaSandbox, set your Daytona API key (or pass api_key= to the
constructor):
Verify Installation¶
Basic (LocalBackend)¶
from pydantic_ai_backends import LocalBackend
backend = LocalBackend(root_dir=".")
backend.write("test.txt", "Hello from pydantic-ai-backend!")
print(backend.read("test.txt"))
With Console Toolset¶
from dataclasses import dataclass
from pydantic_ai import Agent
from pydantic_ai_backends import LocalBackend, create_console_toolset
@dataclass
class Deps:
backend: LocalBackend
backend = LocalBackend(root_dir=".", enable_execute=False)
toolset = create_console_toolset(include_execute=False)
agent = Agent("openai:gpt-4o-mini", deps_type=Deps)
agent = agent.with_toolset(toolset)
result = agent.run_sync("List files in current directory", deps=Deps(backend=backend))
print(result.output)
With Docker¶
from pydantic_ai_backends import DockerSandbox
sandbox = DockerSandbox(image="python:3.12-slim")
sandbox.write("/workspace/hello.py", "print('Hello from Docker!')")
result = sandbox.execute("python /workspace/hello.py")
print(result.output) # "Hello from Docker!"
sandbox.stop()
Troubleshooting¶
Import Errors¶
Ensure you have the correct Python version:
Docker Permission Denied¶
On Linux, add your user to the docker group:
Then log out and back in.
pydantic-ai Not Found¶
If using console toolset, install with the [console] extra:
Choosing a low-level container runtime¶
Docker does not run containers itself. It hands each one to an OCI runtime —
by default runc — which is the process that actually creates the namespaces and
executes your code. That runtime is swappable, per container, and it is the only
knob that changes how strong a sandbox's isolation is rather than how much CPU or
memory it gets.
Register the ones you want with the daemon in /etc/docker/daemon.json, then name
one per sandbox or per runtime alias:
{
"runtimes": {
"crun": { "path": "/usr/bin/crun" },
"runsc": { "path": "/usr/local/bin/runsc" }
}
}
from pydantic_ai_backends import DockerSandbox
# gVisor: syscalls handled in userspace, not by the host kernel.
sandbox = DockerSandbox(image="python:3.12-slim", oci_runtime="runsc")
Or service-wide and per runtime in sandboxd, where a runtime's own choice wins
over the service default:
from pydantic_ai_backends.remote.server import SandboxRuntime, SandboxdConfig
config = SandboxdConfig(
token="...",
runtimes={
"shell": SandboxRuntime(image="alpine:3"),
# The runtime allowed to install packages off the network is the one
# worth paying gVisor's I/O overhead for.
"scraping": SandboxRuntime(
runtime="python-scraping",
network_mode="bridge",
oci_runtime="runsc",
),
},
default_runtime="shell",
)
A runtime the daemon does not know about makes it refuse to start the
container, which surfaces as a 502 from POST /sessions. That is why the
default here is None — the daemon's own choice — rather than something we pick
on your behalf. GET /policy and the dashboard report which one is in force.
What each one buys you¶
| Runtime | Isolation | Cost | Use when |
|---|---|---|---|
runc (default) |
Namespaces + cgroups, host kernel shared | none | Code you trust |
crun |
Same as runc |
none — it is faster | Always, if available |
runsc (gVisor) |
Syscalls intercepted in userspace | ~10–30% on I/O-heavy work, near zero on compute | Untrusted, model-written code |
kata |
Own kernel per container, in a microVM | ~200ms start instead of milliseconds | Hard multi-tenant boundaries |
crun: a faster runc, for free¶
crun is a drop-in reimplementation of runc in C rather than Go. It is not a
different isolation model — a crun container is exactly as isolated as a runc
one — it is the same thing with less overhead per container operation, because
there is no Go runtime to start and no garbage collector.
Published comparisons put the container lifecycle around 20% faster. Make it the daemon's default and every sandbox gets it with no code change at all:
Install it with apt install crun or dnf install crun, then
systemctl restart docker.
Two caveats worth knowing before you size a host around it. The widely quoted
"~3 GB of RAM per node" figure comes from Kubernetes measurements comparing
CRI-O + crun against containerd + runc, so it includes the CRI layer and does
not transfer to a Docker deployment unchanged. And under Docker the persistent
per-container process is containerd-shim-runc-v2, which crun does not replace
— so expect the win to show up mainly in start and stop latency, and measure your
own host before promising anyone a density number.
Next Steps¶
- Core Concepts - Learn the fundamentals
- Local Backend Example - Start with local files
- API Reference - Complete API documentation