mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 05:37:20 +08:00
feat: add host network support for V1 DockerSandboxService (#12445)
Co-authored-by: mkdev11 <MkDev11@users.noreply.github.com> Co-authored-by: hieptl <hieptl.developer@gmail.com>
This commit is contained in:
@@ -84,6 +84,7 @@ class DockerSandboxService(SandboxService):
|
||||
extra_hosts: dict[str, str] = field(default_factory=dict)
|
||||
docker_client: docker.DockerClient = field(default_factory=get_docker_client)
|
||||
startup_grace_seconds: int = STARTUP_GRACE_SECONDS
|
||||
use_host_network: bool = False
|
||||
|
||||
def _find_unused_port(self) -> int:
|
||||
"""Find an unused port on the host machine."""
|
||||
@@ -140,36 +141,61 @@ class DockerSandboxService(SandboxService):
|
||||
env = self._get_container_env_vars(container)
|
||||
session_api_key = env.get(SESSION_API_KEY_VARIABLE)
|
||||
|
||||
# Get the first exposed port mapping
|
||||
# Get the exposed port mappings
|
||||
exposed_urls = []
|
||||
port_bindings = container.attrs.get('NetworkSettings', {}).get('Ports', {})
|
||||
if port_bindings:
|
||||
for container_port, host_bindings in port_bindings.items():
|
||||
if host_bindings:
|
||||
host_port = host_bindings[0]['HostPort']
|
||||
exposed_port = next(
|
||||
(
|
||||
exposed_port
|
||||
for exposed_port in self.exposed_ports
|
||||
if container_port
|
||||
== f'{exposed_port.container_port}/tcp'
|
||||
),
|
||||
None,
|
||||
|
||||
# Check if container is using host network mode
|
||||
network_mode = container.attrs.get('HostConfig', {}).get('NetworkMode', '')
|
||||
is_host_network = network_mode == 'host'
|
||||
|
||||
if is_host_network:
|
||||
# Host network mode: container ports are directly accessible on host
|
||||
for exposed_port in self.exposed_ports:
|
||||
host_port = exposed_port.container_port
|
||||
url = self.container_url_pattern.format(port=host_port)
|
||||
|
||||
# VSCode URLs require the api_key and working dir
|
||||
if exposed_port.name == VSCODE:
|
||||
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
|
||||
|
||||
exposed_urls.append(
|
||||
ExposedUrl(
|
||||
name=exposed_port.name,
|
||||
url=url,
|
||||
port=host_port,
|
||||
)
|
||||
if exposed_port:
|
||||
url = self.container_url_pattern.format(port=host_port)
|
||||
|
||||
# VSCode URLs require the api_key and working dir
|
||||
if exposed_port.name == VSCODE:
|
||||
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
|
||||
|
||||
exposed_urls.append(
|
||||
ExposedUrl(
|
||||
name=exposed_port.name,
|
||||
url=url,
|
||||
port=host_port,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Bridge network mode: use port bindings
|
||||
port_bindings = container.attrs.get('NetworkSettings', {}).get(
|
||||
'Ports', {}
|
||||
)
|
||||
if port_bindings:
|
||||
for container_port, host_bindings in port_bindings.items():
|
||||
if host_bindings:
|
||||
host_port = int(host_bindings[0]['HostPort'])
|
||||
matching_port = next(
|
||||
(
|
||||
ep
|
||||
for ep in self.exposed_ports
|
||||
if container_port == f'{ep.container_port}/tcp'
|
||||
),
|
||||
None,
|
||||
)
|
||||
if matching_port:
|
||||
url = self.container_url_pattern.format(port=host_port)
|
||||
|
||||
# VSCode URLs require the api_key and working dir
|
||||
if matching_port.name == VSCODE:
|
||||
url += f'/?tkn={session_api_key}&folder={container.attrs["Config"]["WorkingDir"]}'
|
||||
|
||||
exposed_urls.append(
|
||||
ExposedUrl(
|
||||
name=matching_port.name,
|
||||
url=url,
|
||||
port=host_port,
|
||||
)
|
||||
)
|
||||
|
||||
return SandboxInfo(
|
||||
id=container.name,
|
||||
@@ -300,6 +326,15 @@ class DockerSandboxService(SandboxService):
|
||||
self, sandbox_spec_id: str | None = None, sandbox_id: str | None = None
|
||||
) -> SandboxInfo:
|
||||
"""Start a new sandbox."""
|
||||
# Warn about port collision risk when using host network mode with multiple sandboxes
|
||||
if self.use_host_network and self.max_num_sandboxes > 1:
|
||||
_logger.warning(
|
||||
'Host network mode is enabled with max_num_sandboxes > 1. '
|
||||
'Multiple sandboxes will attempt to bind to the same ports, '
|
||||
'which may cause port collision errors. Consider setting '
|
||||
'max_num_sandboxes=1 when using host network mode.'
|
||||
)
|
||||
|
||||
# Enforce sandbox limits by cleaning up old sandboxes
|
||||
await self.pause_old_sandboxes(self.max_num_sandboxes - 1)
|
||||
|
||||
@@ -335,12 +370,20 @@ class DockerSandboxService(SandboxService):
|
||||
env_vars[ALLOW_CORS_ORIGINS_VARIABLE] = self.web_url
|
||||
|
||||
# Prepare port mappings and add port environment variables
|
||||
port_mappings = {}
|
||||
for exposed_port in self.exposed_ports:
|
||||
host_port = self._find_unused_port()
|
||||
port_mappings[exposed_port.container_port] = host_port
|
||||
# Add port as environment variable
|
||||
env_vars[exposed_port.name] = str(host_port)
|
||||
# When using host network, container ports are directly accessible on the host
|
||||
# so we use the container ports directly instead of mapping to random host ports
|
||||
port_mappings: dict[int, int] | None = None
|
||||
if self.use_host_network:
|
||||
# Host network mode: container ports are directly accessible
|
||||
for exposed_port in self.exposed_ports:
|
||||
env_vars[exposed_port.name] = str(exposed_port.container_port)
|
||||
else:
|
||||
# Bridge network mode: map container ports to random host ports
|
||||
port_mappings = {}
|
||||
for exposed_port in self.exposed_ports:
|
||||
host_port = self._find_unused_port()
|
||||
port_mappings[exposed_port.container_port] = host_port
|
||||
env_vars[exposed_port.name] = str(host_port)
|
||||
|
||||
# Prepare labels
|
||||
labels = {
|
||||
@@ -356,6 +399,12 @@ class DockerSandboxService(SandboxService):
|
||||
for mount in self.mounts
|
||||
}
|
||||
|
||||
# Determine network mode
|
||||
network_mode = 'host' if self.use_host_network else None
|
||||
|
||||
if self.use_host_network:
|
||||
_logger.info(f'Starting sandbox {container_name} with host network mode')
|
||||
|
||||
try:
|
||||
# Create and start the container
|
||||
container = self.docker_client.containers.run( # type: ignore[call-overload]
|
||||
@@ -374,7 +423,12 @@ class DockerSandboxService(SandboxService):
|
||||
init=True,
|
||||
# Allow agent-server containers to resolve host.docker.internal
|
||||
# and other custom hostnames for LAN deployments
|
||||
extra_hosts=self.extra_hosts if self.extra_hosts else None,
|
||||
# Note: extra_hosts is not needed with host network mode
|
||||
extra_hosts=self.extra_hosts
|
||||
if self.extra_hosts and not self.use_host_network
|
||||
else None,
|
||||
# Network mode: 'host' for host networking, None for default bridge
|
||||
network_mode=network_mode,
|
||||
)
|
||||
|
||||
sandbox_info = await self._container_to_sandbox_info(container)
|
||||
@@ -526,6 +580,21 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
|
||||
'before it is considered an error'
|
||||
),
|
||||
)
|
||||
use_host_network: bool = Field(
|
||||
default=os.getenv('SANDBOX_USE_HOST_NETWORK', '').lower()
|
||||
in (
|
||||
'true',
|
||||
'1',
|
||||
'yes',
|
||||
),
|
||||
description=(
|
||||
'Whether to use host networking mode for sandbox containers. '
|
||||
'When enabled, containers share the host network namespace, '
|
||||
'making all container ports directly accessible on the host. '
|
||||
'This is useful for reverse proxy setups where dynamic port mapping '
|
||||
'is problematic. Configure via OH_SANDBOX_USE_HOST_NETWORK environment variable.'
|
||||
),
|
||||
)
|
||||
|
||||
async def inject(
|
||||
self, state: InjectorState, request: Request | None = None
|
||||
@@ -558,4 +627,5 @@ class DockerSandboxServiceInjector(SandboxServiceInjector):
|
||||
web_url=web_url,
|
||||
extra_hosts=self.extra_hosts,
|
||||
startup_grace_seconds=self.startup_grace_seconds,
|
||||
use_host_network=self.use_host_network,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user