diff --git a/apps/api/src/api/tool_instances.py b/apps/api/src/api/tool_instances.py index 1472f75..05e9d54 100644 --- a/apps/api/src/api/tool_instances.py +++ b/apps/api/src/api/tool_instances.py @@ -1834,7 +1834,8 @@ async def start_instance( instance_port, ) tunnel_info = start_cloudflared_tunnel( - host_port=instance.port, + container_name=instance.container_name or instance.name, + port=instance_port, ) instance.tunnel_id = tunnel_info["pid"] instance.public_url = tunnel_info["url"] @@ -2041,12 +2042,15 @@ async def restart_instance( "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 if tool_type.interface_type == "web": # Create new temporary tunnel try: tunnel_info = start_cloudflared_tunnel( - host_port=instance.port or 0, + container_name=instance.container_name or instance.name, + port=instance_port, ) instance.tunnel_id = tunnel_info["pid"] instance.public_url = tunnel_info["url"] @@ -2279,9 +2283,16 @@ async def recreate_tunnel_endpoint( "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: tunnel_info = recreate_tunnel( - host_port=instance.port or 0, + container_name=instance.container_name or instance.name, + port=instance_port, old_pid=instance.tunnel_id, ) instance.tunnel_id = tunnel_info["pid"] diff --git a/apps/api/src/services/docker.py b/apps/api/src/services/docker.py index 625a6a9..d179d92 100644 --- a/apps/api/src/services/docker.py +++ b/apps/api/src/services/docker.py @@ -385,15 +385,16 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int: def start_cloudflared_tunnel( - host_port: int, timeout: int = 30 + container_name: str, port: int, timeout: int = 30 ) -> dict[str, str]: - """Start a temporary Cloudflare tunnel to localhost. + """Start a temporary Cloudflare tunnel for a container. Uses 'cloudflared tunnel --url' to create a temporary tunnel with a random trycloudflare.com URL. Args: - host_port: Host-mapped port number (e.g. from find_free_port) + container_name: Name of the Docker container to tunnel to + port: Port number the container listens on timeout: Maximum seconds to wait for tunnel URL Returns: @@ -404,8 +405,8 @@ def start_cloudflared_tunnel( logger = logging.getLogger(__name__) - # First verify the container is accessible via the host-mapped port - logger.info("Checking connectivity to localhost:%d...", host_port) + # First verify the container is accessible + logger.info("Checking connectivity to %s:%d...", container_name, port) for attempt in range(10): check = subprocess.run( [ @@ -415,7 +416,7 @@ def start_cloudflared_tunnel( "/dev/null", "-w", "%{http_code}", - f"http://localhost:{host_port}", + f"http://{container_name}:{port}", ], capture_output=True, text=True, @@ -429,13 +430,13 @@ def start_cloudflared_tunnel( time.sleep(1) else: logger.warning( - "localhost:%d not responding to curl checks", host_port + "Container %s:%d not responding to curl checks", container_name, port ) # Run cloudflared in background, capture output - logger.info("Starting cloudflared tunnel to http://localhost:%d", host_port) + logger.info("Starting cloudflared tunnel to http://%s:%d", container_name, port) proc = subprocess.Popen( - ["cloudflared", "tunnel", "--url", f"http://localhost:{host_port}"], + ["cloudflared", "tunnel", "--url", f"http://{container_name}:{port}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, @@ -490,14 +491,15 @@ def stop_cloudflared_tunnel(pid: str) -> None: def recreate_tunnel( - host_port: int, old_pid: str | None = None + container_name: str, port: int, old_pid: str | None = None ) -> dict[str, str]: """Recreate a temporary Cloudflare tunnel. Stops the old tunnel (if pid provided) and starts a new one. Args: - host_port: Host-mapped port number (e.g. from find_free_port) + container_name: Name of the Docker container to tunnel to + port: Port number the container listens on old_pid: Optional PID of the old tunnel process to stop Returns: @@ -506,7 +508,7 @@ def recreate_tunnel( if old_pid: stop_cloudflared_tunnel(old_pid) - return start_cloudflared_tunnel(host_port) + return start_cloudflared_tunnel(container_name, port) def check_tunnel_health(url: str, timeout: int = 10) -> dict[str, Any]: diff --git a/apps/api/src/services/lifecycle_hooks.py b/apps/api/src/services/lifecycle_hooks.py index ddc4530..17c07e8 100644 --- a/apps/api/src/services/lifecycle_hooks.py +++ b/apps/api/src/services/lifecycle_hooks.py @@ -120,9 +120,9 @@ async def publish_lifecycle_event( # Create notification for instance owner (fire-and-forget) # Skip intermediate "starting" notifications — only notify on terminal states # (failed or successful attempts) - _is_starting_intermediate = ( - event_type == "instance.started" and (status or instance.status) == "starting" - ) + _is_starting_intermediate = event_type == "instance.started" and ( + status or instance.status + ) == "starting" if _is_starting_intermediate: return