fix: recreate tunnel uses container IP directly for reliable connectivity

Old instances may have auto-generated Docker Compose container names
that don't match instance.name.lower(), causing DNS resolution failures
for the tunnel. Also, old instances may not be on the backend network.

- apps/api/src/services/docker.py: add get_container_ip_on_network() and
  is_container_on_network() helpers
- apps/api/src/services/tunnel.py: start_tunnel() and recreate_tunnel() now
  accept an optional target_url parameter to override the default name-based URL
- apps/api/src/api/tool_instances.py: recreate_tunnel_endpoint now:
  1. Looks up the tool container (by stored container_id or name)
  2. Ensures it's connected to the backend network
  3. Gets the container's IP on that network
  4. Passes the IP as the explicit tunnel target

This guarantees the tunnel can reach the tool container regardless of
naming or network state.

Quality gates: ruff clean
This commit is contained in:
2026-05-30 14:29:55 +02:00
parent 321b4e3d0e
commit 4cc433a1b8
3 changed files with 129 additions and 5 deletions
+49
View File
@@ -45,13 +45,16 @@ from src.services.config_profile_resolver import (
resolve_profile,
)
from src.services.docker import (
connect_container_to_network,
ensure_instance_directory,
execute_compose_command,
find_free_port,
get_backend_network_name,
get_container_id,
get_container_ip_on_network,
get_container_logs,
get_container_status,
is_container_on_network,
render_compose_template,
sort_volumes_by_specificity,
wait_for_container_running,
@@ -2459,10 +2462,56 @@ async def recreate_tunnel_endpoint(
detail="Tool type not found for this instance",
)
# Find the tool container — try stored ID first, then fall back to name lookup
tool_container_id = instance.container_id
if not tool_container_id:
tool_container_id = get_container_id(instance.name.lower())
if not tool_container_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Could not find running container for this instance",
)
# Ensure the tool container is on the backend network so the tunnel can reach it
network_name = get_backend_network_name()
if not is_container_on_network(tool_container_id, network_name):
logger.debug(
"Connecting container %s to network %s for tunnel access",
tool_container_id,
network_name,
)
connected = connect_container_to_network(tool_container_id, network_name)
if not connected:
logger.warning(
"Failed to connect container %s to network %s",
tool_container_id,
network_name,
)
# Use the container's IP on the backend network as the tunnel target.
# This is more reliable than name-based DNS, especially for old instances
# whose container name may differ from instance.name.lower().
target_ip = get_container_ip_on_network(tool_container_id, network_name)
if target_ip:
target_url = f"http://{target_ip}:{tool_type.default_port or 0}"
logger.debug(
"Using container IP %s as tunnel target for instance %s",
target_ip,
instance.id,
)
else:
target_url = None
logger.warning(
"Could not get container IP for %s, falling back to name resolution",
tool_container_id,
)
try:
tunnel_info = recreate_tunnel(
instance_name=instance.name,
container_port=tool_type.default_port or 0,
target_url=target_url,
)
instance.tunnel_id = tunnel_info["container_name"]
instance.public_url = tunnel_info["url"]