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:
2026-05-30 13:52:05 +02:00
parent eeb7d9a1b2
commit 9cab8c7bc7
2 changed files with 32 additions and 23 deletions
+18 -15
View File
@@ -1,10 +1,10 @@
"""Clean tunnel service using host-network cloudflared containers.
"""Clean tunnel service using cloudflared containers on the backend network.
Design:
- Each tunnel runs as a Docker container on the host network.
- cloudflared connects to localhost:{published_port}, leveraging Docker's
port forwarding. No container name resolution is required.
- Tunnels are named predictably (tunnel-{instance_name}) for start/stop.
- Each tunnel runs as a Docker container on the same 'backend' network as the API.
- cloudflared connects to the tool container by its Docker Compose service name
(e.g. http://code-server-headquarter-34837cd3:8443).
- This avoids host port conflicts and DNS resolution issues.
"""
import logging
@@ -15,6 +15,7 @@ from typing import Any
logger = logging.getLogger(__name__)
TUNNEL_IMAGE = "cloudflare/cloudflared:latest"
TUNNEL_NETWORK = "backend"
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(
instance_name: str, published_port: int, timeout: int = 30
instance_name: str, container_port: int, timeout: int = 30
) -> dict[str, str]:
"""Start a temporary Cloudflare tunnel for an instance.
Args:
instance_name: The tool instance name (used to derive tunnel container name).
published_port: The host port Docker forwards to the container.
instance_name: The tool instance name (used for tunnel naming).
container_port: The port the tool container listens on internally.
timeout: Seconds to wait for the tunnel URL.
Returns:
@@ -96,19 +97,22 @@ def start_tunnel(
tunnel_name = _tunnel_container_name(instance_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 = [
"docker",
"run",
"-d",
"--network",
"host",
TUNNEL_NETWORK,
"--name",
tunnel_name,
TUNNEL_IMAGE,
"tunnel",
"--no-autoupdate",
"--url",
f"http://localhost:{published_port}",
target_url,
]
logger.debug("Running: %s", " ".join(cmd))
@@ -148,7 +152,6 @@ def start_tunnel(
__import__("time").sleep(0.5)
if not url:
# Capture final state for debugging
stdout, stderr = _get_container_logs(tunnel_name)
combined_logs = stdout + "\n" + stderr
exit_code = _get_container_exit_code(tunnel_name)
@@ -160,10 +163,10 @@ def start_tunnel(
)
logger.info(
"Tunnel %s started for %s on port %d %s",
"Tunnel %s started for %s %s (%s)",
tunnel_name,
instance_name,
published_port,
target_url,
url,
)
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)
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."""
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]: