fix: run tunnel containers on backend network with container name DNS
The host-network tunnel approach had issues because localhost inside the tunnel container wasn't reaching the host-published ports correctly. This reverts to running cloudflared as a Docker container on the 'backend' network, where Docker DNS resolves container names reliably. The tunnel connects to http://{container_name}:{container_port}. - apps/api/src/services/tunnel.py: use --network backend instead of host - apps/api/src/api/tool_instances.py: pass container_port (default_port) instead of published_port (host port) to tunnel functions Quality gates: ruff clean
This commit is contained in:
@@ -1962,12 +1962,11 @@ async def start_instance(
|
|||||||
"error": f"Tool type '{instance.tool_type_id}' not found",
|
"error": f"Tool type '{instance.tool_type_id}' not found",
|
||||||
}
|
}
|
||||||
|
|
||||||
published_port = instance.port or 0
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Tool type for instance %s: name=%s, published_port=%s, interface_type=%s",
|
"Tool type for instance %s: name=%s, container_port=%s, interface_type=%s",
|
||||||
instance.id,
|
instance.id,
|
||||||
tool_type.name,
|
tool_type.name,
|
||||||
published_port,
|
tool_type.default_port or 0,
|
||||||
tool_type.interface_type,
|
tool_type.interface_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1976,13 +1975,13 @@ async def start_instance(
|
|||||||
# Create temporary Cloudflare tunnel for public access
|
# Create temporary Cloudflare tunnel for public access
|
||||||
try:
|
try:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Creating tunnel for instance %s (port=%d)",
|
"Creating tunnel for instance %s (container_port=%d)",
|
||||||
instance.id,
|
instance.id,
|
||||||
published_port,
|
tool_type.default_port or 0,
|
||||||
)
|
)
|
||||||
tunnel_info = start_tunnel(
|
tunnel_info = start_tunnel(
|
||||||
instance_name=instance.name,
|
instance_name=instance.name,
|
||||||
published_port=published_port,
|
container_port=tool_type.default_port or 0,
|
||||||
)
|
)
|
||||||
instance.tunnel_id = tunnel_info["container_name"]
|
instance.tunnel_id = tunnel_info["container_name"]
|
||||||
instance.public_url = tunnel_info["url"]
|
instance.public_url = tunnel_info["url"]
|
||||||
@@ -2204,7 +2203,7 @@ async def restart_instance(
|
|||||||
try:
|
try:
|
||||||
tunnel_info = start_tunnel(
|
tunnel_info = start_tunnel(
|
||||||
instance_name=instance.name,
|
instance_name=instance.name,
|
||||||
published_port=instance.port or 0,
|
container_port=tool_type.default_port or 0,
|
||||||
)
|
)
|
||||||
instance.tunnel_id = tunnel_info["container_name"]
|
instance.tunnel_id = tunnel_info["container_name"]
|
||||||
instance.public_url = tunnel_info["url"]
|
instance.public_url = tunnel_info["url"]
|
||||||
@@ -2437,10 +2436,17 @@ async def recreate_tunnel_endpoint(
|
|||||||
"message": "Tunnel is already healthy",
|
"message": "Tunnel is already healthy",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tool_type = await session.get(ToolType, instance.tool_type_id)
|
||||||
|
if not tool_type:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Tool type not found for this instance",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tunnel_info = recreate_tunnel(
|
tunnel_info = recreate_tunnel(
|
||||||
instance_name=instance.name,
|
instance_name=instance.name,
|
||||||
published_port=instance.port or 0,
|
container_port=tool_type.default_port or 0,
|
||||||
)
|
)
|
||||||
instance.tunnel_id = tunnel_info["container_name"]
|
instance.tunnel_id = tunnel_info["container_name"]
|
||||||
instance.public_url = tunnel_info["url"]
|
instance.public_url = tunnel_info["url"]
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"""Clean tunnel service using host-network cloudflared containers.
|
"""Clean tunnel service using cloudflared containers on the backend network.
|
||||||
|
|
||||||
Design:
|
Design:
|
||||||
- Each tunnel runs as a Docker container on the host network.
|
- Each tunnel runs as a Docker container on the same 'backend' network as the API.
|
||||||
- cloudflared connects to localhost:{published_port}, leveraging Docker's
|
- cloudflared connects to the tool container by its Docker Compose service name
|
||||||
port forwarding. No container name resolution is required.
|
(e.g. http://code-server-headquarter-34837cd3:8443).
|
||||||
- Tunnels are named predictably (tunnel-{instance_name}) for start/stop.
|
- This avoids host port conflicts and DNS resolution issues.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -15,6 +15,7 @@ from typing import Any
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
TUNNEL_IMAGE = "cloudflare/cloudflared:latest"
|
TUNNEL_IMAGE = "cloudflare/cloudflared:latest"
|
||||||
|
TUNNEL_NETWORK = "backend"
|
||||||
|
|
||||||
|
|
||||||
def _tunnel_container_name(instance_name: str) -> str:
|
def _tunnel_container_name(instance_name: str) -> str:
|
||||||
@@ -79,13 +80,13 @@ def _get_container_exit_code(tunnel_name: str) -> int | None:
|
|||||||
|
|
||||||
|
|
||||||
def start_tunnel(
|
def start_tunnel(
|
||||||
instance_name: str, published_port: int, timeout: int = 30
|
instance_name: str, container_port: int, timeout: int = 30
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Start a temporary Cloudflare tunnel for an instance.
|
"""Start a temporary Cloudflare tunnel for an instance.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
instance_name: The tool instance name (used to derive tunnel container name).
|
instance_name: The tool instance name (used for tunnel naming).
|
||||||
published_port: The host port Docker forwards to the container.
|
container_port: The port the tool container listens on internally.
|
||||||
timeout: Seconds to wait for the tunnel URL.
|
timeout: Seconds to wait for the tunnel URL.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -96,19 +97,22 @@ def start_tunnel(
|
|||||||
tunnel_name = _tunnel_container_name(instance_name)
|
tunnel_name = _tunnel_container_name(instance_name)
|
||||||
_cleanup_stale_tunnel(tunnel_name)
|
_cleanup_stale_tunnel(tunnel_name)
|
||||||
|
|
||||||
|
# Target the tool container by name on the backend network
|
||||||
|
target_url = f"http://{instance_name.lower()}:{container_port}"
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"docker",
|
"docker",
|
||||||
"run",
|
"run",
|
||||||
"-d",
|
"-d",
|
||||||
"--network",
|
"--network",
|
||||||
"host",
|
TUNNEL_NETWORK,
|
||||||
"--name",
|
"--name",
|
||||||
tunnel_name,
|
tunnel_name,
|
||||||
TUNNEL_IMAGE,
|
TUNNEL_IMAGE,
|
||||||
"tunnel",
|
"tunnel",
|
||||||
"--no-autoupdate",
|
"--no-autoupdate",
|
||||||
"--url",
|
"--url",
|
||||||
f"http://localhost:{published_port}",
|
target_url,
|
||||||
]
|
]
|
||||||
|
|
||||||
logger.debug("Running: %s", " ".join(cmd))
|
logger.debug("Running: %s", " ".join(cmd))
|
||||||
@@ -148,7 +152,6 @@ def start_tunnel(
|
|||||||
__import__("time").sleep(0.5)
|
__import__("time").sleep(0.5)
|
||||||
|
|
||||||
if not url:
|
if not url:
|
||||||
# Capture final state for debugging
|
|
||||||
stdout, stderr = _get_container_logs(tunnel_name)
|
stdout, stderr = _get_container_logs(tunnel_name)
|
||||||
combined_logs = stdout + "\n" + stderr
|
combined_logs = stdout + "\n" + stderr
|
||||||
exit_code = _get_container_exit_code(tunnel_name)
|
exit_code = _get_container_exit_code(tunnel_name)
|
||||||
@@ -160,10 +163,10 @@ def start_tunnel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Tunnel %s started for %s on port %d → %s",
|
"Tunnel %s started for %s → %s (%s)",
|
||||||
tunnel_name,
|
tunnel_name,
|
||||||
instance_name,
|
instance_name,
|
||||||
published_port,
|
target_url,
|
||||||
url,
|
url,
|
||||||
)
|
)
|
||||||
return {"url": url, "container_name": tunnel_name}
|
return {"url": url, "container_name": tunnel_name}
|
||||||
@@ -176,10 +179,10 @@ def stop_tunnel(instance_name: str) -> None:
|
|||||||
logger.debug("Stopped and removed tunnel container %s", tunnel_name)
|
logger.debug("Stopped and removed tunnel container %s", tunnel_name)
|
||||||
|
|
||||||
|
|
||||||
def recreate_tunnel(instance_name: str, published_port: int) -> dict[str, str]:
|
def recreate_tunnel(instance_name: str, container_port: int) -> dict[str, str]:
|
||||||
"""Recreate a tunnel for an instance."""
|
"""Recreate a tunnel for an instance."""
|
||||||
stop_tunnel(instance_name)
|
stop_tunnel(instance_name)
|
||||||
return start_tunnel(instance_name, published_port)
|
return start_tunnel(instance_name, container_port)
|
||||||
|
|
||||||
|
|
||||||
def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]:
|
def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user