AutoGen’s code execution capability lets agents write, run, and iterate on code autonomously. Running that code in Docker isolates it from your host, prevents dependency conflicts, and gives you a clean slate for every session. This guide walks through a production-ready Docker setup for AutoGen code execution, with n4n.ai handling the LLM routing behind a single OpenAI-compatible endpoint.
Step 1: Choose your Docker image strategy
You have two paths. The quickest is Microsoft’s official mcr.microsoft.com/autogen/autogen-agentchat image, which bundles Python, common data-science libraries, and the AutoGen runtime. The alternative — and the one I recommend for anything beyond a demo — is building your own image so you control the exact Python version, system packages, and pinned dependencies.
Create a Dockerfile in your project root:
# Dockerfile
FROM python:3.11-slim
# System deps for common data-science wheels
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user for safety
RUN useradd -m -u 1000 autogen && \
mkdir -p /home/autogen/workspace && \
chown -R autogen:autogen /home/autogen
WORKDIR /home/autogen/workspace
USER autogen
# Pin dependencies for reproducibility
COPY --chown=autogen:autogen requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Default command keeps container alive for exec sessions
CMD ["sleep", "infinity"]
Your requirements.txt should pin everything:
# requirements.txt
autogen-agentchat==0.2.0
autogen-ext==0.2.0
pandas==2.2.2
numpy==1.26.4
matplotlib==3.8.4
requests==2.31.0
Build and tag it:
docker build -t autogen-sandbox:0.2.0 .
Step 2: Configure the Docker command-line executor
AutoGen’s DockerCommandLineCodeExecutor spins up a container per session, mounts a workspace directory, and tears it down when the agent finishes. Create a small wrapper module so your agent code stays clean:
# docker_executor.py
import os
import tempfile
from pathlib import Path
from autogen_ext.code_executors.docker import DockerCommandLineCodeExecutor
def create_executor(
image: str = "autogen-sandbox:0.2.0",
workspace: Path | None = None,
timeout: int = 120,
) -> DockerCommandLineCodeExecutor:
"""
Returns a configured Docker executor.
Each call creates a fresh container; the workspace persists across
executions within the same session.
"""
if workspace is None:
workspace = Path(tempfile.mkdtemp(prefix="autogen_ws_"))
# Bind-mount the workspace so the container can read/write files
bind_mounts = {str(workspace.resolve()): "/home/autogen/workspace"}
return DockerCommandLineCodeExecutor(
image=image,
bind_mounts=bind_mounts,
working_dir="/home/autogen/workspace",
timeout=timeout,
# Auto-remove container on exit; keeps host clean
auto_remove=True,
)
The bind_mounts mapping is the critical piece: the host directory becomes /home/autogen/workspace inside the container, matching the WORKDIR in your Dockerfile. The executor writes generated scripts there, runs them, and returns stdout/stderr to the agent.
Step 3: Wire the executor into an AutoGen agent
AutoGen 0.2+ uses the CodeExecutor protocol. Attach your executor to a UserProxyAgent (or any agent that implements code_executor):
# agent_setup.py
from autogen_agentchat.agents import AssistantAgent, UserProxyAgent
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.messages import TextMessage
from autogen_ext.models.openai import OpenAIChatCompletionClient
from docker_executor import create_executor
# n4n.ai provides one OpenAI-compatible endpoint for 240+ models.
# Set OPENAI_BASE_URL to your n4n.ai gateway and OPENAI_API_KEY to your key.
model_client = OpenAIChatCompletionClient(
model="gpt-4o-mini", # routed through n4n.ai
base_url=os.getenv("OPENAI_BASE_URL"),
api_key=os.getenv("OPENAI_API_KEY"),
)
executor = create_executor()
coding_agent = AssistantAgent(
name="coder",
model_client=model_client,
system_message=(
"You write Python code to solve tasks. "
"Use the workspace directory for any file I/O. "
"Return only the code block, no markdown formatting."
),
)
user_proxy = UserProxyAgent(
name="user_proxy",
code_executor=executor,
# Let the agent run up to 3 code blocks per turn
max_consecutive_auto_reply=3,
)
team = RoundRobinGroupChat([coding_agent, user_proxy], max_turns=6)
Note the max_consecutive_auto_reply — without it, a single turn could spin into an unbounded loop of code execution. Three is a sensible default; tune it per task complexity.
Step 4: Run a verification task
Create a script that exercises the full loop: agent writes code, Docker executes it, result flows back, agent decides next step.
# verify.py
import asyncio
import os
from agent_setup import team
async def main():
task = (
"Create a file called 'fibonacci.py' that defines a function "
"fib(n) returning the nth Fibonacci number. Then write a second "
"script 'test_fib.py' that imports it and prints fib(10). "
"Run the test script and show me the output."
)
async for msg in team.run_stream(task=task):
if isinstance(msg, TextMessage):
print(f"[{msg.source}] {msg.content}")
if __name__ == "__main__":
asyncio.run(main())
Run it:
export OPENAI_BASE_URL="https://api.n4n.ai/v1"
export OPENAI_API_KEY="your-n4n-key"
python verify.py
You should see the agent write two files, execute test_fib.py, and print 55. The workspace directory (printed in logs if you add debug logging) will contain both scripts — useful for post-mortem debugging.
Step 5: Harden the sandbox for production
The default setup is functional but permissive. Apply these guards before exposing the endpoint to untrusted input.
Limit container resources
Add resource constraints to the executor creation:
# docker_executor.py (updated create_executor)
return DockerCommandLineCodeExecutor(
image=image,
bind_mounts=bind_mounts,
working_dir="/home/autogen/workspace",
timeout=timeout,
auto_remove=True,
# Resource limits — adjust to your workload
container_kwargs={
"mem_limit": "512m",
"cpu_count": 1,
"pids_limit": 64,
"network_mode": "none", # no outbound network
},
)
network_mode: "none" prevents the container from reaching the internet or your internal network. If your tasks need to fetch data, create a sidecar proxy or pre-bake datasets into the image.
Drop capabilities and enforce read-only root filesystem
Extend container_kwargs:
container_kwargs={
"mem_limit": "512m",
"cpu_count": 1,
"pids_limit": 64,
"network_mode": "none",
"cap_drop": ["ALL"],
"security_opt": ["no-new-privileges:true"],
"read_only": True,
"tmpfs": {
"/tmp": "size=100m,noexec,nosuid,nodev",
"/home/autogen/workspace": "size=200m,exec",
},
},
The read_only root filesystem forces all writes into the tmpfs mounts. The workspace gets exec so Python can run scripts there; /tmp is noexec to block binary execution from world-writable directories.
Use a dedicated Docker network (optional)
If you run multiple executor instances concurrently, put them on an isolated bridge network with no gateway:
docker network create --driver bridge --internal autogen-sandbox
Then add network="autogen-sandbox" to container_kwargs. Containers can talk to each other if you need multi-container tasks, but nothing reaches the host or internet.
Step 6: Handle streaming and long-running executions
The default timeout=120 covers most data-science tasks. For streaming results (e.g., a training loop that logs progress), increase the timeout and set up a callback:
# agent_setup.py (add to user_proxy)
from autogen_ext.code_executors.docker import DockerCommandLineCodeExecutor
def on_code_execution_start(executor: DockerCommandLineCodeExecutor, code: str):
print(f"[exec] Running {len(code)} bytes of code...")
def on_code_execution_end(executor: DockerCommandLineCodeExecutor, result):
print(f"[exec] Exit code: {result.exit_code}, stdout: {result.output[:200]}...")
executor = create_executor(timeout=600)
executor.on_code_execution_start = on_code_execution_start
executor.on_code_execution_end = on_code_execution_end
These hooks let you surface progress to a UI or log aggregation system without parsing agent messages.
Step 7: Persist workspace across sessions (when you need it)
By default, create_executor creates a fresh temp directory each call. For multi-turn conversations where the agent builds on prior artifacts, reuse the same workspace:
# session_manager.py
from pathlib import Path
from docker_executor import create_executor
class SessionManager:
def __init__(self, base_dir: Path = Path("./sessions")):
self.base_dir = base_dir
self.base_dir.mkdir(parents=True, exist_ok=True)
def get_executor(self, session_id: str):
ws = self.base_dir / session_id
ws.mkdir(exist_ok=True)
return create_executor(workspace=ws)
Pass the same session_id for all turns in a conversation. Clean up old sessions with a cron job or TTL policy.
Step 8: Verify the hardened setup
Run the verification script again. Confirm:
- Container starts and exits cleanly (check
docker ps -a— nothing should linger). - No outbound network calls succeed (try
requests.get("http://example.com")in the agent task — it should fail). - Memory/CPU limits trigger on runaway tasks (write a
while True: passloop and watch the container OOM kill). - Workspace files persist only where expected.
Add a quick smoke test to CI:
# test_smoke.py
import pytest
from agent_setup import team
@pytest.mark.asyncio
async def test_fibonacci_task():
result = await team.run(
task="Write and run a script that prints the 10th Fibonacci number."
)
assert "55" in str(result)
Run with pytest test_smoke.py -v. This catches image regressions, executor misconfiguration, and routing failures early.
Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
docker: Error response from daemon: pull access denied |
Image not built or wrong tag | Run docker build -t autogen-sandbox:0.2.0 . and verify with docker images |
Permission denied on workspace writes |
UID/GID mismatch | Ensure Dockerfile useradd -u 1000 matches host user, or use docker run --user $(id -u):$(id -g) |
TimeoutError after 120s |
Task exceeds default timeout | Increase timeout in create_executor or optimize the agent’s code |
ModuleNotFoundError inside container |
Missing package in requirements.txt | Rebuild image with the missing dependency pinned |
| Agent loops indefinitely | max_consecutive_auto_reply too high or missing |
Set a low value (3-5) and add a termination condition in the system message |
What this gives you
A reproducible, isolated code execution environment that:
- Spins up in ~2 seconds on a warm host (image cached)
- Enforces memory, CPU, and network boundaries
- Persists artifacts only where you decide
- Routes LLM calls through a single endpoint that handles fallback and metering automatically
The same pattern scales to Kubernetes — swap DockerCommandLineCodeExecutor for a custom executor that pods kubectl run with identical resource limits. The agent logic stays untouched.