fix: improve tunnel diagnostics and add --no-autoupdate

- Remove --rm from docker run so failed containers persist for inspection
- Add --no-autoupdate flag to prevent cloudflared from exiting on auto-update
- Capture both stdout and stderr from docker logs
- Check container exit code during wait loop; fail fast with logs if container exits early
- Include exit code in timeout error message for easier debugging
This commit is contained in:
2026-05-30 12:41:37 +02:00
parent 6cf06d2380
commit eeb7d9a1b2
2 changed files with 83 additions and 40 deletions
-3
View File
@@ -380,6 +380,3 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int:
return port
raise RuntimeError(f"No free port found in range {start}-{end}")
+83 -37
View File
@@ -39,7 +39,48 @@ def _ensure_image() -> None:
logger.warning("Failed to pull %s: %s", TUNNEL_IMAGE, pull.stderr)
def start_tunnel(instance_name: str, published_port: int, timeout: int = 30) -> dict[str, str]:
def _cleanup_stale_tunnel(tunnel_name: str) -> None:
"""Remove any existing tunnel container with this name."""
subprocess.run(
["docker", "stop", "-t", "3", tunnel_name],
capture_output=True,
text=True,
)
subprocess.run(
["docker", "rm", "-f", tunnel_name],
capture_output=True,
text=True,
)
def _get_container_logs(tunnel_name: str) -> tuple[str, str]:
"""Get stdout and stderr logs from a container."""
result = subprocess.run(
["docker", "logs", tunnel_name],
capture_output=True,
text=True,
)
return result.stdout, result.stderr
def _get_container_exit_code(tunnel_name: str) -> int | None:
"""Get exit code of a container if it has exited."""
result = subprocess.run(
["docker", "inspect", "-f", "{{.State.ExitCode}}", tunnel_name],
capture_output=True,
text=True,
)
if result.returncode == 0:
try:
return int(result.stdout.strip())
except ValueError:
pass
return None
def start_tunnel(
instance_name: str, published_port: int, timeout: int = 30
) -> dict[str, str]:
"""Start a temporary Cloudflare tunnel for an instance.
Args:
@@ -53,60 +94,69 @@ def start_tunnel(instance_name: str, published_port: int, timeout: int = 30) ->
_ensure_image()
tunnel_name = _tunnel_container_name(instance_name)
# Clean up any stale tunnel container with this name
subprocess.run(
["docker", "stop", "-t", "3", tunnel_name],
capture_output=True,
text=True,
)
subprocess.run(
["docker", "rm", tunnel_name],
capture_output=True,
text=True,
)
_cleanup_stale_tunnel(tunnel_name)
cmd = [
"docker", "run", "-d", "--rm",
"--network", "host",
"--name", tunnel_name,
"docker",
"run",
"-d",
"--network",
"host",
"--name",
tunnel_name,
TUNNEL_IMAGE,
"tunnel", "--url", f"http://localhost:{published_port}",
"tunnel",
"--no-autoupdate",
"--url",
f"http://localhost:{published_port}",
]
logger.debug("Running: %s", " ".join(cmd))
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
raise RuntimeError(
f"Failed to start tunnel container {tunnel_name}: {proc.stderr}"
)
container_id = proc.stdout.strip()
logger.debug("Tunnel container started: %s", container_id)
# Wait for URL to appear in logs
url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
start_time = __import__("time").time()
url: str | None = None
logs_result = subprocess.run(
["docker", "logs", tunnel_name],
capture_output=True,
text=True,
)
combined_logs = ""
while __import__("time").time() - start_time < timeout:
match = url_pattern.search(logs_result.stdout)
stdout, stderr = _get_container_logs(tunnel_name)
combined_logs = stdout + "\n" + stderr
match = url_pattern.search(combined_logs)
if match:
url = match.group(0)
break
# Check if container exited early
exit_code = _get_container_exit_code(tunnel_name)
if exit_code is not None and exit_code != 0:
_cleanup_stale_tunnel(tunnel_name)
raise RuntimeError(
f"Tunnel container {tunnel_name} exited with code {exit_code}. "
f"Logs:\n{combined_logs[-3000:]}"
)
__import__("time").sleep(0.5)
logs_result = subprocess.run(
["docker", "logs", tunnel_name],
capture_output=True,
text=True,
)
if not url:
subprocess.run(["docker", "stop", "-t", "3", tunnel_name], capture_output=True)
# 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)
_cleanup_stale_tunnel(tunnel_name)
raise RuntimeError(
f"Tunnel {tunnel_name} did not produce a URL within {timeout}s. "
f"cloudflared logs:\n{logs_result.stdout[-2000:]}"
f"Exit code: {exit_code}. Logs:\n{combined_logs[-3000:]}"
)
logger.info(
@@ -120,14 +170,10 @@ def start_tunnel(instance_name: str, published_port: int, timeout: int = 30) ->
def stop_tunnel(instance_name: str) -> None:
"""Stop the tunnel container for an instance."""
"""Stop and remove the tunnel container for an instance."""
tunnel_name = _tunnel_container_name(instance_name)
subprocess.run(
["docker", "stop", "-t", "5", tunnel_name],
capture_output=True,
text=True,
)
logger.debug("Stopped tunnel container %s", tunnel_name)
_cleanup_stale_tunnel(tunnel_name)
logger.debug("Stopped and removed tunnel container %s", tunnel_name)
def recreate_tunnel(instance_name: str, published_port: int) -> dict[str, str]: