Use this file to discover all available pages before exploring further.
The docker sandboxed agent server demonstrates how to run agents in isolated Docker containers using DockerWorkspace.This provides complete isolation from the host system, making it ideal for production deployments, testing, and executing untrusted code safely.Use DockerWorkspace with a pre-built agent server image for the fastest startup. When you need to build your own image from a base image, switch to DockerDevWorkspace.
the Docker sandbox image ships with features configured in the Dockerfile (e.g., secure defaults and services like VSCode and VNC exposed behind well-defined ports), which are not available in the local (non-Docker) agent server.
The DockerWorkspace uses a context manager to automatically handle container lifecycle:
with DockerWorkspace( # use pre-built image for faster startup (recommended) server_image="ghcr.io/openhands/agent-server:latest-python", host_port=8010, platform=detect_platform(),) as workspace: # Container is running here # Work with the workspace pass# Container is automatically stopped and cleaned up here
The example includes platform detection to ensure the correct Docker image is built and used:
def detect_platform(): """Detects the correct Docker platform string.""" machine = platform.machine().lower() if "arm" in machine or "aarch64" in machine: return "linux/arm64" return "linux/amd64"
This ensures compatibility across different CPU architectures (Intel/AMD vs ARM/Apple Silicon).
Use DockerWorkspace when you can rely on the official pre-built images for the agent server. Switch to DockerDevWorkspace when you need to build or customize the image on-demand (slower startup, requires the SDK source tree and Docker build support).
# ✅ Fast: Use pre-built image (recommended)DockerWorkspace( server_image="ghcr.io/openhands/agent-server:latest-python", host_port=8010,)# 🛠️ Custom: Build on the fly (requires SDK tooling)DockerDevWorkspace( base_image="nikolaik/python-nodejs:python3.12-nodejs22", host_port=8010, target="source",)
import osimport platformimport timefrom pydantic import SecretStrfrom openhands.sdk import ( LLM, Conversation, RemoteConversation, get_logger,)from openhands.tools.preset.default import get_default_agentfrom openhands.workspace import DockerWorkspacelogger = get_logger(__name__)# 1) Ensure we have LLM API keyapi_key = os.getenv("LLM_API_KEY")assert api_key is not None, "LLM_API_KEY environment variable is not set."llm = LLM( usage_id="agent", model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key),)def detect_platform(): """Detects the correct Docker platform string.""" machine = platform.machine().lower() if "arm" in machine or "aarch64" in machine: return "linux/arm64" return "linux/amd64"def get_server_image(): """Get the server image tag, using PR-specific image in CI.""" platform_str = detect_platform() arch = "arm64" if "arm64" in platform_str else "amd64" # If GITHUB_SHA is set (e.g. running in CI of a PR), use that to ensure consistency # Otherwise, use the latest image from main github_sha = os.getenv("GITHUB_SHA") if github_sha: return f"ghcr.io/openhands/agent-server:{github_sha[:7]}-python-{arch}" return "ghcr.io/openhands/agent-server:latest-python"# 2) Create a Docker-based remote workspace that will set up and manage# the Docker container automatically. Use `DockerWorkspace` with a pre-built# image or `DockerDevWorkspace` to automatically build the image on-demand.# with DockerDevWorkspace(# # dynamically build agent-server image# base_image="nikolaik/python-nodejs:python3.13-nodejs22",# host_port=8010,# platform=detect_platform(),# ) as workspace:server_image = get_server_image()logger.info(f"Using server image: {server_image}")with DockerWorkspace( # use pre-built image for faster startup server_image=server_image, host_port=8010, platform=detect_platform(),) as workspace: # 3) Create agent agent = get_default_agent( llm=llm, cli_mode=True, ) # 4) Set up callback collection received_events: list = [] last_event_time = {"ts": time.time()} def event_callback(event) -> None: event_type = type(event).__name__ logger.info(f"🔔 Callback received event: {event_type}\n{event}") received_events.append(event) last_event_time["ts"] = time.time() # 5) Test the workspace with a simple command result = workspace.execute_command( "echo 'Hello from sandboxed environment!' && pwd" ) logger.info( f"Command '{result.command}' completed with exit code {result.exit_code}" ) logger.info(f"Output: {result.stdout}") conversation = Conversation( agent=agent, workspace=workspace, callbacks=[event_callback], ) assert isinstance(conversation, RemoteConversation) try: logger.info(f"\n📋 Conversation ID: {conversation.state.id}") logger.info("📝 Sending first message...") conversation.send_message( "Read the current repo and write 3 facts about the project into FACTS.txt." ) logger.info("🚀 Running conversation...") conversation.run() logger.info("✅ First task completed!") logger.info(f"Agent status: {conversation.state.execution_status}") # Wait for events to settle (no events for 2 seconds) logger.info("⏳ Waiting for events to stop...") while time.time() - last_event_time["ts"] < 2.0: time.sleep(0.1) logger.info("✅ Events have stopped") logger.info("🚀 Running conversation again...") conversation.send_message("Great! Now delete that file.") conversation.run() logger.info("✅ Second task completed!") cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost print(f"EXAMPLE_COST: {cost}") finally: print("\n🧹 Cleaning up conversation...") conversation.close()
You can run the example code as-is.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.
VS Code with Docker demonstrates how to enable VS Code Web integration in a Docker-sandboxed environment. This allows you to access a full VS Code editor running in the container, making it easy to inspect, edit, and manage files that the agent is working with.
The workspace is configured with extra ports for VS Code access:
with DockerWorkspace( server_image="ghcr.io/openhands/agent-server:latest-python", host_port=18010, platform="linux/arm64", # or "linux/amd64" depending on your architecture extra_ports=True, # Expose extra ports for VSCode and VNC) as workspace: """Extra ports allows you to access VSCode at localhost:18011"""
The extra_ports=True setting exposes:
Port host_port+1: VS Code Web interface (host_port + 1)
Port host_port+2: VNC viewer for visual access
If you need to customize the agent-server image, swap in DockerDevWorkspace with the same parameters and provide base_image/target to build on demand.
import osimport platformimport timeimport httpxfrom pydantic import SecretStrfrom openhands.sdk import LLM, Conversation, get_loggerfrom openhands.sdk.conversation.impl.remote_conversation import RemoteConversationfrom openhands.tools.preset.default import get_default_agentfrom openhands.workspace import DockerWorkspacelogger = get_logger(__name__)api_key = os.getenv("LLM_API_KEY")assert api_key is not None, "LLM_API_KEY environment variable is not set."llm = LLM( usage_id="agent", model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key),)# Create a Docker-based remote workspace with extra ports for VSCode accessdef detect_platform(): """Detects the correct Docker platform string.""" machine = platform.machine().lower() if "arm" in machine or "aarch64" in machine: return "linux/arm64" return "linux/amd64"def get_server_image(): """Get the server image tag, using PR-specific image in CI.""" platform_str = detect_platform() arch = "arm64" if "arm64" in platform_str else "amd64" # If GITHUB_SHA is set (e.g. running in CI of a PR), use that to ensure consistency # Otherwise, use the latest image from main github_sha = os.getenv("GITHUB_SHA") if github_sha: return f"ghcr.io/openhands/agent-server:{github_sha[:7]}-python-{arch}" return "ghcr.io/openhands/agent-server:latest-python"server_image = get_server_image()logger.info(f"Using server image: {server_image}")with DockerWorkspace( server_image=server_image, host_port=18010, platform=detect_platform(), extra_ports=True, # Expose extra ports for VSCode and VNC) as workspace: """Extra ports allows you to access VSCode at localhost:18011""" # Create agent agent = get_default_agent( llm=llm, cli_mode=True, ) # Set up callback collection received_events: list = [] last_event_time = {"ts": time.time()} def event_callback(event) -> None: event_type = type(event).__name__ logger.info(f"🔔 Callback received event: {event_type}\n{event}") received_events.append(event) last_event_time["ts"] = time.time() # Create RemoteConversation using the workspace conversation = Conversation( agent=agent, workspace=workspace, callbacks=[event_callback], ) assert isinstance(conversation, RemoteConversation) logger.info(f"\n📋 Conversation ID: {conversation.state.id}") logger.info("📝 Sending first message...") conversation.send_message("Create a simple Python script that prints Hello World") conversation.run() # Get VSCode URL with token vscode_port = (workspace.host_port or 8010) + 1 try: response = httpx.get( f"{workspace.host}/api/vscode/url", params={"workspace_dir": workspace.working_dir}, ) vscode_data = response.json() vscode_url = vscode_data.get("url", "").replace( "localhost:8001", f"localhost:{vscode_port}" ) except Exception: # Fallback if server route not available folder = ( f"/{workspace.working_dir}" if not str(workspace.working_dir).startswith("/") else str(workspace.working_dir) ) vscode_url = f"http://localhost:{vscode_port}/?folder={folder}" # Wait for user to explore VSCode y = None while y != "y": y = input( "\n" "Because you've enabled extra_ports=True in DockerDevWorkspace, " "you can open VSCode Web to see the workspace.\n\n" f"VSCode URL: {vscode_url}\n\n" "The VSCode should have the OpenHands settings extension installed:\n" " - Dark theme enabled\n" " - Auto-save enabled\n" " - Telemetry disabled\n" " - Auto-updates disabled\n\n" "Press 'y' and Enter to exit and terminate the workspace.\n" ">> " )
You can run the example code as-is.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.
Browser with Docker demonstrates how to enable browser automation capabilities in a Docker-sandboxed environment. This allows agents to browse websites, interact with web content, and perform web automation tasks while maintaining complete isolation from your host system.
The workspace is configured with extra ports for browser access:
with DockerWorkspace( server_image="ghcr.io/openhands/agent-server:latest-python", host_port=8010, platform=detect_platform(), extra_ports=True, # Expose extra ports for VSCode and VNC) as workspace: """Extra ports allows you to check localhost:8012 for VNC"""
The extra_ports=True setting exposes additional ports for:
Port host_port+1: VS Code Web interface
Port host_port+2: VNC viewer for browser visualization
If you need to pre-build a custom browser image, replace DockerWorkspace with DockerDevWorkspace and provide base_image/target to build before launch.
Browser tools are enabled by setting cli_mode=False:
# Create agent with browser tools enabledagent = get_default_agent( llm=llm, cli_mode=False, # CLI mode = False will enable browser tools)
When cli_mode=False, the agent gains access to browser automation tools for web interaction.When VNC is available and extra_ports=True, the browser will be opened in the VNC desktop to visualize agent’s work. You can watch the browser in real-time via VNC. Demo video:
import osimport platformimport timefrom pydantic import SecretStrfrom openhands.sdk import LLM, Conversation, get_loggerfrom openhands.sdk.conversation.impl.remote_conversation import RemoteConversationfrom openhands.tools.preset.default import get_default_agentfrom openhands.workspace import DockerWorkspacelogger = get_logger(__name__)api_key = os.getenv("LLM_API_KEY")assert api_key is not None, "LLM_API_KEY environment variable is not set."llm = LLM( usage_id="agent", model=os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929"), base_url=os.getenv("LLM_BASE_URL"), api_key=SecretStr(api_key),)def detect_platform(): """Detects the correct Docker platform string.""" machine = platform.machine().lower() if "arm" in machine or "aarch64" in machine: return "linux/arm64" return "linux/amd64"def get_server_image(): """Get the server image tag, using PR-specific image in CI.""" platform_str = detect_platform() arch = "arm64" if "arm64" in platform_str else "amd64" # If GITHUB_SHA is set (e.g. running in CI of a PR), use that to ensure consistency # Otherwise, use the latest image from main github_sha = os.getenv("GITHUB_SHA") if github_sha: return f"ghcr.io/openhands/agent-server:{github_sha[:7]}-python-{arch}" return "ghcr.io/openhands/agent-server:latest-python"# Create a Docker-based remote workspace with extra ports for browser access.# Use `DockerWorkspace` with a pre-built image or `DockerDevWorkspace` to# automatically build the image on-demand.# with DockerDevWorkspace(# # dynamically build agent-server image# base_image="nikolaik/python-nodejs:python3.13-nodejs22",# host_port=8010,# platform=detect_platform(),# ) as workspace:server_image = get_server_image()logger.info(f"Using server image: {server_image}")with DockerWorkspace( server_image=server_image, host_port=8011, platform=detect_platform(), extra_ports=True, # Expose extra ports for VSCode and VNC) as workspace: """Extra ports allows you to check localhost:8012 for VNC""" # Create agent with browser tools enabled agent = get_default_agent( llm=llm, cli_mode=False, # CLI mode = False will enable browser tools ) # Set up callback collection received_events: list = [] last_event_time = {"ts": time.time()} def event_callback(event) -> None: event_type = type(event).__name__ logger.info(f"🔔 Callback received event: {event_type}\n{event}") received_events.append(event) last_event_time["ts"] = time.time() # Create RemoteConversation using the workspace conversation = Conversation( agent=agent, workspace=workspace, callbacks=[event_callback], ) assert isinstance(conversation, RemoteConversation) logger.info(f"\n📋 Conversation ID: {conversation.state.id}") logger.info("📝 Sending first message...") conversation.send_message( "Could you go to https://openhands.dev/ blog page and summarize main " "points of the latest blog?" ) conversation.run() cost = conversation.conversation_stats.get_combined_metrics().accumulated_cost print(f"EXAMPLE_COST: {cost}") if os.getenv("CI"): logger.info( "CI environment detected; skipping interactive prompt and closing workspace." # noqa: E501 ) else: # Wait for user confirm to exit when running locally y = None while y != "y": y = input( "Because you've enabled extra_ports=True in DockerDevWorkspace, " "you can open a browser tab to see the *actual* browser OpenHands " "is interacting with via VNC.\n\n" "Link: http://localhost:8012/vnc.html?autoconnect=1&resize=remote\n\n" "Press 'y' and Enter to exit and terminate the workspace.\n" ">> " )
You can run the example code as-is.
The model name should follow the LiteLLM convention: provider/model_name (e.g., anthropic/claude-sonnet-4-5-20250929, openai/gpt-4o).
The LLM_API_KEY should be the API key for your chosen provider.
ChatGPT Plus/Pro subscribers: You can use LLM.subscription_login() to authenticate with your ChatGPT account and access Codex models without consuming API credits. See the LLM Subscriptions guide for details.