DEV Community

Jangwook Kim
Jangwook Kim

Posted on Originally published at effloow.com

OpenHands Self-Hosted Runtime Review: Docker Isolation, Agent Cost, and the Production Reality

Why We Evaluated OpenHands

We brought OpenHands into our evaluation queue because buying another coding-agent seat does not solve the infrastructure question facing engineering teams. We wanted to know whether we could operate the execution layer ourselves, connect it to a model provider of our choice, and retain control over source code, runtime images, network policy, and usage accounting.

We often see OpenHands described as an open-source Devin alternative, but that description is too shallow for an architecture review. The interesting component is not the chat interface. It is the runtime responsible for turning model-generated actions into shell commands, file mutations, Python execution, and observations.

We traced that path through the OpenHands runtime architecture. The backend launches a purpose-built runtime container, starts an action execution server inside it, sends actions over an HTTP interface, and receives structured observations. The model remains outside that execution boundary. The container is where potentially destructive commands run.

That separation is useful. It gives us an explicit control point for image policy, workspace mounts, resource limits, process cleanup, and audit logging. It also means OpenHands is not merely an application that happens to use Docker. Docker is part of its security and scheduling model.

The image workflow is more sophisticated than a naïve build-per-task design. We found three levels of runtime tags:

  • A versioned tag tied to the OpenHands version and base image.
  • A lock tag derived from the base image and dependency files.
  • A source tag incorporating the OpenHands source hash.

When an exact source image exists, the runtime can reuse it. When only the dependency-compatible layer exists, it can copy changed source without repeating the slow dependency installation path. That design should reduce repeated build work, although we would still pin immutable image digests rather than rely on a moving tag in production.

The project’s 90,000-plus-star GitHub repository made it impossible to dismiss as a weekend prototype. We treated that popularity as an adoption signal, not evidence of secure isolation or predictable operating cost.

Our central questions were therefore practical:

  1. Can we reproduce the documented local deployment without hidden services?
  2. Does the sandbox protect the host under a realistic threat model?
  3. Can we measure model spend per conversation?
  4. What happens when multiple sandboxes need ports, storage, and cleanup?
  5. At what team size does self-hosting become cheaper than buying hosted seats?

We also wanted to separate what we could verify from what required a functioning Docker daemon and paid model credentials. That distinction became important almost immediately.

Hands-On Walkthrough: Setup, Execution & Output

We started with the supported local prerequisites: Python 3.12, uv, and a Docker installation with access to the Docker socket. We treated the 4 GB host-memory recommendation as a setup baseline, not a measured per-sandbox footprint. It is a minimum host recommendation, not a capacity-planning number.

The shortest launcher path was:

uv tool install openhands --python 3.12
openhands serve --mount-cwd
Enter fullscreen mode Exit fullscreen mode

The --mount-cwd option mounts the current repository into the workspace. That convenience has a direct security consequence: an agent with write access can modify anything exposed through that mount.

For a more controlled reproduction, we prepared a command with explicit application and agent-server versions rather than using latest:

docker run -it --rm --pull=always \
  -e AGENT_SERVER_IMAGE_REPOSITORY=ghcr.io/openhands/agent-server \
  -e AGENT_SERVER_IMAGE_TAG=1.26.0-python \
  -e LOG_ALL_EVENTS=true \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v "$HOME/.openhands:/.openhands" \
  -p 3000:3000 \
  --add-host host.docker.internal:host-gateway \
  --name openhands-app \
  docker.openhands.dev/openhands/openhands:1.8
Enter fullscreen mode Exit fullscreen mode

This command exposes the web application on http://localhost:3000. More importantly, it mounts /var/run/docker.sock into the application container so OpenHands can create and manage sandbox containers.

We hit a hard environmental stop before a useful runtime benchmark: our available runner did not expose a Docker daemon. We could inspect the launch configuration, but we did not run a deployment-contract test or measure container startup latency, memory consumption, concurrent sandbox throughput, or cleanup behavior on that runner. We did not replace those missing measurements with invented numbers.

For a host with Docker available, the SDK-level sandbox path is straightforward:

import platform

from openhands.workspace import DockerWorkspace


def detect_platform() -> str:
    machine = platform.machine().lower()
    if "arm" in machine or "aarch64" in machine:
        return "linux/arm64"
    return "linux/amd64"


with DockerWorkspace(
    server_image="ghcr.io/openhands/agent-server:1.26.0-python",
    host_port=8010,
    platform=detect_platform(),
) as workspace:
    result = workspace.execute_command(
        "printf 'sandbox-ready\\n' && pwd && python --version"
    )
    print(
        {
            "command": result.command,
            "exit_code": result.exit_code,
            "stdout": result.stdout,
            "stderr": result.stderr,
        }
    )
Enter fullscreen mode Exit fullscreen mode

The expected response shape below is simulated to make the reproduction contract explicit. It is not presented as a measurement from our Docker-unavailable runner:

{
  "command": "printf 'sandbox-ready\\n' && pwd && python --version",
  "exit_code": 0,
  "stdout": "sandbox-ready\n/workspace\nPython 3.12.x\n",
  "stderr": ""
}
Enter fullscreen mode Exit fullscreen mode

DockerWorkspace is the right choice when we can use a prebuilt agent-server image. DockerDevWorkspace builds from a selected base image and is appropriate when we need additional compilers, system packages, or internal tooling. We would not use the development workspace on a latency-sensitive request path because image construction adds an avoidable startup stage.

A real agent conversation introduces another dependency: an LLM credential. For our API-backed conversation design, we use LLM_API_KEY and LiteLLM-style model identifiers such as anthropic/... or openai/..., with accumulated cost reporting available through:

cost = (
    conversation.conversation_stats
    .get_combined_metrics()
    .accumulated_cost
)
Enter fullscreen mode Exit fullscreen mode

We used that metric as the required accounting surface in our production design. We would record it by conversation, repository, model, tenant, and completion state. We would not treat it as a complete bill until reconciling it against provider invoices, retries, failed requests, cache behavior, and any model calls made outside the conversation object.

A local model endpoint can use a placeholder API-key value when its server does not authenticate requests. That removes the paid-provider key dependency, but it does not remove inference cost. GPU depreciation, idle capacity, model serving, and weaker task completion can easily cost more than API tokens.

Deployment Limitations and Operational Risks

The first failure was simple and decisive: without Docker, there was no meaningful OpenHands sandbox benchmark. A mock Docker client could verify Python control flow, but it could not verify namespaces, mounts, port binding, image pulls, process termination, or actual container cleanup. We rejected a mock-only result as production evidence.

The second issue was the Docker socket. The architecture isolates agent commands in a child container, but the parent OpenHands application requires control of the host Docker daemon in the direct deployment. On a conventional Linux host, access to that socket is effectively privileged host control. If the application container is compromised, the sandbox boundary does not protect the Docker host from the controller.

Our workaround is architectural rather than cosmetic:

  • Run OpenHands workers on dedicated nodes or disposable virtual machines.
  • Do not colocate them with databases, CI secrets, or customer-facing services.
  • Restrict outbound network access from both controller and sandbox.
  • Mount only the repository needed for the task.
  • Prefer read-only mounts plus an explicit writable worktree where possible.
  • Apply CPU, memory, process, and execution-time limits.
  • Treat the Docker host as replaceable after hostile workloads.

OpenHands supports bind mounts, named volumes, and overlay copy-on-write behavior. Overlay mode requires a bind mount marked with :overlay and a writable SANDBOX_VOLUME_OVERLAYS host directory. If we do not configure that directory, OpenHands skips the overlay path. We would fail startup on that condition rather than silently continue with different write semantics.

When planning the reverse-proxy deployment, we identified another design problem. Each sandbox normally receives random host ports, while generated browser URLs default to a localhost pattern. Setting OH_WEB_URL does not rewrite those sandbox URLs; it affects the allowed origin rather than the hostname and port used to reach the container.

We found two imperfect options.

The first is to set OH_SANDBOX_CONTAINER_URL_PATTERN to a public hostname containing {port}. That retains a random port for each sandbox, forcing the edge layer to forward a dynamic range.

The second is host networking:

export AGENT_SERVER_USE_HOST_NETWORK=true
Enter fullscreen mode Exit fullscreen mode

This exposes fixed ports, including agent-server port 8000 and editor-related port 8001. The trade-off is severe: every sandbox tries to bind the same ports, so only one sandbox can run on that host without collision. We would not use host networking for a multi-tenant production worker.

We also found that editor capabilities vary by image. We should not assume portable SDK images include VS Code, VNC, a fixed editor port, or /api/vscode/url. The extra_ports=True pattern belongs to selected legacy editor-capable images, where host_port + 1 and host_port + 2 may expose VS Code and VNC. We would feature-detect those services instead of deriving URLs unconditionally.

Finally, moving tags are a reproducibility risk. ghcr.io/openhands/agent-server:latest-python is convenient for exploration, but production rollouts need pinned versions or, preferably, image digests. We would test the application image, agent-server image, base image, SDK, and model configuration as one release unit.

What This Evaluation Could Not Verify

We could not measure OpenHands startup latency, throughput, or memory use because our runner lacked Docker. The available setup material also does not provide a reproducible production benchmark.

Scaling and Cost vs. Alternatives

What we can evaluate is the cost and scaling shape.

Option Execution ownership Isolation model Model cost Operational burden Best fit
OpenHands self-hosted Our team Docker sandbox managed by OpenHands Paid API or local inference High Teams needing model and runtime control
E2B-style managed sandbox Provider Remote managed sandbox Model plus sandbox usage Medium Fast deployment without worker operations
Custom Docker runner Our team Whatever we build and audit Paid API or local inference Very high Specialized policies or narrow workflows
Hosted coding-agent seat Vendor Vendor-managed Usually bundled or plan-dependent Low Teams prioritizing speed over control
IDE agent Developer machine Local permissions or editor controls Subscription or API Low to medium Interactive individual development

OpenHands removes per-seat runtime licensing from the equation, but it does not make agent execution free. Our monthly model is:

OpenHands monthly cost =
    worker infrastructure
  + engineering maintenance
  + observability and storage
  + LLM or local inference
  + security and incident overhead
Enter fullscreen mode Exit fullscreen mode

The corresponding hosted calculation is:

Hosted monthly cost =
    number of seats × effective monthly seat price
  + overages
  + integration overhead
Enter fullscreen mode Exit fullscreen mode

For an illustrative break-even model, assume:

  • Worker infrastructure and logs: $600 per month.
  • Runtime maintenance: 12 engineering hours per month.
  • Loaded engineering rate: $150 per hour.
  • Self-hosted model usage: $120 per active user per month.
  • Hosted alternative: $500 per active user per month.

That produces $2,400 in fixed monthly self-hosting cost and a $380 per-user advantage:

Break-even users = $2,400 / ($500 - $120) = 6.32
Enter fullscreen mode Exit fullscreen mode

Under those assumptions, self-hosting becomes cheaper at approximately seven active users. This is a planning example, not a quoted Devin price or an observed OpenHands bill. If maintenance rises to 30 hours, or if agents burn tokens through repeated tool loops, the break-even point moves sharply upward.

Agent-loop cost is the largest variable. A task is not one model call. The agent may inspect files, invoke tools, ingest observations, revise its plan, run tests, encounter errors, and ask the model again. Long command output also returns to context and can increase subsequent request cost.

We would enforce four controls before opening access:

  1. Maximum model spend per conversation.
  2. Maximum loop iterations and wall-clock duration.
  3. Output truncation and artifact storage outside the prompt.
  4. Per-team and per-repository budgets with hard rejection.

For teams building this into a broader internal platform, our AI infrastructure services cover the worker isolation, observability, and cost-control layer that the default local launcher does not provide. We also maintain a broader set of implementation references in our tools collection.

Our Final Verdict: When to Deploy, When to Skip

OpenHands has a credible runtime design. We like the separation between the agent backend and the in-container action server, the prebuilt DockerWorkspace path, the customizable development workspace, and the exposed conversation-cost metric.

We do not consider the default single-host Docker deployment a complete production security boundary. The Docker socket, repository mounts, dynamic ports, image compatibility, outbound network policy, and cleanup behavior all become our responsibility the moment we self-host.

Deploy this if:

  • We need control over model providers, runtime images, and source-code location.
  • We can dedicate isolated worker hosts to untrusted agent execution.
  • We have enough active usage to absorb recurring platform maintenance.
  • We can pin and test the full application and agent-server image set.
  • We will enforce token, iteration, time, memory, and network limits.
  • We accept that self-hosting transfers operational risk rather than eliminating cost.

Hold off or avoid it if:

  • We need a turnkey service with contractual support and predictable seats.
  • We plan to expose the host Docker socket on shared infrastructure.
  • We cannot operate dynamic sandbox ports behind our edge architecture.
  • We expect bundled VS Code or VNC capabilities in every SDK image.
  • We lack per-conversation model budgets and provider-bill reconciliation.
  • We need audited multi-tenant isolation stronger than ordinary Docker containers.
  • We need benchmark-backed capacity numbers before running our own Docker tests.

Our recommendation is a guarded deploy for a controlled pilot, not an immediate shared production rollout. Start with a dedicated worker, one pinned image set, outbound-deny networking, disposable repository copies, and a small group of engineers. Measure cold and warm startup separately, record accumulated model cost, test cleanup after forced termination, and attempt deliberate workspace escapes before expanding.

OpenHands can reduce vendor lock-in and give us a useful coding-agent control plane. It does not remove the hard parts. It makes those parts visible—and assigns them to our infrastructure team. Teams that want help validating that trade-off against their own workload can contact us with their concurrency, repository, compliance, and model assumptions.

Top comments (0)