fix: cloudflare tunnel connects to localhost:host_port instead of container_name:container_port

Root cause: start_cloudflared_tunnel was trying to connect to
http://{container_name}:{container_port}, but:
1. The host OS cannot resolve Docker container names
2. cloudflared runs on the host, so it needs the host-mapped port

Changes:
- start_cloudflared_tunnel: changed signature to accept host_port only
- Connects cloudflared to localhost:{host_port} via Docker port mapping
- Connectivity check uses localhost:{host_port}
- recreate_tunnel updated to match new signature
- Callers in tool_instances.py pass instance.port (host port)

Quality gates: pytest 42 passed
This commit is contained in:
2026-05-29 15:13:04 +02:00
parent b11089896a
commit a8fbca9ef5
3 changed files with 18 additions and 31 deletions
+3 -14
View File
@@ -1818,8 +1818,7 @@ async def start_instance(
instance_port, instance_port,
) )
tunnel_info = start_cloudflared_tunnel( tunnel_info = start_cloudflared_tunnel(
container_name=instance.container_name or instance.name, host_port=instance.port,
port=instance_port,
) )
instance.tunnel_id = tunnel_info["pid"] instance.tunnel_id = tunnel_info["pid"]
instance.public_url = tunnel_info["url"] instance.public_url = tunnel_info["url"]
@@ -2026,15 +2025,12 @@ async def restart_instance(
"error": f"Tool type '{tool_type.name if tool_type else 'unknown'}' has no port configured", "error": f"Tool type '{tool_type.name if tool_type else 'unknown'}' has no port configured",
} }
instance_port = tool_type.default_port
# Only create tunnel for web-enabled tools # Only create tunnel for web-enabled tools
if tool_type.interface_type == "web": if tool_type.interface_type == "web":
# Create new temporary tunnel # Create new temporary tunnel
try: try:
tunnel_info = start_cloudflared_tunnel( tunnel_info = start_cloudflared_tunnel(
container_name=instance.container_name or instance.name, host_port=instance.port or 0,
port=instance_port,
) )
instance.tunnel_id = tunnel_info["pid"] instance.tunnel_id = tunnel_info["pid"]
instance.public_url = tunnel_info["url"] instance.public_url = tunnel_info["url"]
@@ -2267,16 +2263,9 @@ async def recreate_tunnel_endpoint(
"message": "Tunnel is already healthy", "message": "Tunnel is already healthy",
} }
# Get tool type for default port
tool_type = await session.get(ToolType, instance.tool_type_id)
instance_port = (
tool_type.default_port if tool_type and tool_type.default_port else 8080
)
try: try:
tunnel_info = recreate_tunnel( tunnel_info = recreate_tunnel(
container_name=instance.container_name or instance.name, host_port=instance.port or 0,
port=instance_port,
old_pid=instance.tunnel_id, old_pid=instance.tunnel_id,
) )
instance.tunnel_id = tunnel_info["pid"] instance.tunnel_id = tunnel_info["pid"]
+12 -14
View File
@@ -385,16 +385,15 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int:
def start_cloudflared_tunnel( def start_cloudflared_tunnel(
container_name: str, port: int, timeout: int = 30 host_port: int, timeout: int = 30
) -> dict[str, str]: ) -> dict[str, str]:
"""Start a temporary Cloudflare tunnel for a container. """Start a temporary Cloudflare tunnel to localhost.
Uses 'cloudflared tunnel --url' to create a temporary tunnel Uses 'cloudflared tunnel --url' to create a temporary tunnel
with a random trycloudflare.com URL. with a random trycloudflare.com URL.
Args: Args:
container_name: Name of the Docker container to tunnel to host_port: Host-mapped port number (e.g. from find_free_port)
port: Port number the container listens on
timeout: Maximum seconds to wait for tunnel URL timeout: Maximum seconds to wait for tunnel URL
Returns: Returns:
@@ -405,8 +404,8 @@ def start_cloudflared_tunnel(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# First verify the container is accessible # First verify the container is accessible via the host-mapped port
logger.info("Checking connectivity to %s:%d...", container_name, port) logger.info("Checking connectivity to localhost:%d...", host_port)
for attempt in range(10): for attempt in range(10):
check = subprocess.run( check = subprocess.run(
[ [
@@ -416,7 +415,7 @@ def start_cloudflared_tunnel(
"/dev/null", "/dev/null",
"-w", "-w",
"%{http_code}", "%{http_code}",
f"http://{container_name}:{port}", f"http://localhost:{host_port}",
], ],
capture_output=True, capture_output=True,
text=True, text=True,
@@ -430,13 +429,13 @@ def start_cloudflared_tunnel(
time.sleep(1) time.sleep(1)
else: else:
logger.warning( logger.warning(
"Container %s:%d not responding to curl checks", container_name, port "localhost:%d not responding to curl checks", host_port
) )
# Run cloudflared in background, capture output # Run cloudflared in background, capture output
logger.info("Starting cloudflared tunnel to http://%s:%d", container_name, port) logger.info("Starting cloudflared tunnel to http://localhost:%d", host_port)
proc = subprocess.Popen( proc = subprocess.Popen(
["cloudflared", "tunnel", "--url", f"http://{container_name}:{port}"], ["cloudflared", "tunnel", "--url", f"http://localhost:{host_port}"],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
text=True, text=True,
@@ -491,15 +490,14 @@ def stop_cloudflared_tunnel(pid: str) -> None:
def recreate_tunnel( def recreate_tunnel(
container_name: str, port: int, old_pid: str | None = None host_port: int, old_pid: str | None = None
) -> dict[str, str]: ) -> dict[str, str]:
"""Recreate a temporary Cloudflare tunnel. """Recreate a temporary Cloudflare tunnel.
Stops the old tunnel (if pid provided) and starts a new one. Stops the old tunnel (if pid provided) and starts a new one.
Args: Args:
container_name: Name of the Docker container to tunnel to host_port: Host-mapped port number (e.g. from find_free_port)
port: Port number the container listens on
old_pid: Optional PID of the old tunnel process to stop old_pid: Optional PID of the old tunnel process to stop
Returns: Returns:
@@ -508,7 +506,7 @@ def recreate_tunnel(
if old_pid: if old_pid:
stop_cloudflared_tunnel(old_pid) stop_cloudflared_tunnel(old_pid)
return start_cloudflared_tunnel(container_name, port) return start_cloudflared_tunnel(host_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]:
+3 -3
View File
@@ -120,9 +120,9 @@ async def publish_lifecycle_event(
# Create notification for instance owner (fire-and-forget) # Create notification for instance owner (fire-and-forget)
# Skip intermediate "starting" notifications — only notify on terminal states # Skip intermediate "starting" notifications — only notify on terminal states
# (failed or successful attempts) # (failed or successful attempts)
_is_starting_intermediate = event_type == "instance.started" and ( _is_starting_intermediate = (
status or instance.status event_type == "instance.started" and (status or instance.status) == "starting"
) == "starting" )
if _is_starting_intermediate: if _is_starting_intermediate:
return return