fix(cloudflared): code-server binds to 127.0.0.1 causing tunnel app error 0
Root cause: code-server (and similar web tools) default to binding to 127.0.0.1 (localhost) inside their containers. This makes them unreachable from the Docker network and from cloudflared, which connects via the container's Docker network name. Changes: - Migration: Update code-server compose_template to include --bind-addr 0.0.0.0:8443 command override - Migration: Update jupyter-notebook compose_template to include --ip=0.0.0.0 flag - Runtime safety net: _ensure_web_bind_address() auto-injects bind address for known web tools (code-server, jupyter-notebook) when compose doesn't already specify a command - Diagnostics: _check_app_binding() compares internal vs external connectivity to detect 127.0.0.1 binding issues - Improved readiness check: 30s timeout, checks HTTP status codes, logs curl stderr for debugging Files: - apps/api/alembic/versions/2026_05_29_fix_web_tool_bind_address.py - apps/api/src/services/docker.py - apps/api/src/api/tool_instances.py - apps/api/tests/integration/test_tool_types_api_extended.py Quality gates: pytest 42 passed (5 pre-existing unrelated failures)
This commit is contained in:
@@ -618,6 +618,62 @@ def _modify_compose_file(
|
||||
compose_file.write_text(yaml.dump(compose_data, default_flow_style=False))
|
||||
|
||||
|
||||
def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None:
|
||||
"""Auto-inject bind address for known web tools that default to 127.0.0.1.
|
||||
|
||||
Many web tools (code-server, jupyter) bind to localhost by default,
|
||||
making them inaccessible from the Docker network. This function detects
|
||||
known tool images and injects the correct --bind-addr or --ip flag.
|
||||
"""
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
KNOWN_BIND_FIXES = {
|
||||
"code-server": "--bind-addr 0.0.0.0:8443",
|
||||
"jupyter-notebook": "start-notebook.sh --ip=0.0.0.0",
|
||||
}
|
||||
|
||||
bind_command = KNOWN_BIND_FIXES.get(tool_type_name)
|
||||
if not bind_command:
|
||||
return
|
||||
|
||||
compose_file = Path(compose_path)
|
||||
if not compose_file.exists():
|
||||
return
|
||||
|
||||
content = compose_file.read_text()
|
||||
compose_data = yaml.safe_load(content)
|
||||
|
||||
if not compose_data or "services" not in compose_data:
|
||||
return
|
||||
|
||||
for service_config in compose_data["services"].values():
|
||||
# Skip if command is already overridden
|
||||
if "command" in service_config:
|
||||
return
|
||||
|
||||
image = service_config.get("image", "")
|
||||
if not image:
|
||||
return
|
||||
|
||||
# Check if the image matches a known tool
|
||||
if tool_type_name == "code-server" and (
|
||||
"code-server" in image or "coder" in image
|
||||
):
|
||||
service_config["command"] = bind_command
|
||||
break
|
||||
if tool_type_name == "jupyter-notebook" and (
|
||||
"jupyter" in image or "notebook" in image
|
||||
):
|
||||
service_config["command"] = bind_command
|
||||
break
|
||||
|
||||
compose_file.write_text(yaml.dump(compose_data, default_flow_style=False))
|
||||
logger.info(
|
||||
"Injected bind address for %s: %s", tool_type_name, bind_command
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{project_id}/repositories/{repo_id}/instances",
|
||||
summary="Create tool instance",
|
||||
@@ -854,7 +910,7 @@ services:
|
||||
)
|
||||
|
||||
# Determine home directory for path expansion
|
||||
home_dir = get_manifest_home_dir(manifest)
|
||||
_home_dir = get_manifest_home_dir(manifest)
|
||||
|
||||
image_tag = compute_image_tag(tool_type.name, manifest)
|
||||
|
||||
@@ -1550,6 +1606,10 @@ async def start_instance(
|
||||
# Sanitize compose file to remove invalid port mappings from old instances
|
||||
_sanitize_compose_file(instance.compose_path)
|
||||
|
||||
# Auto-fix bind address for known web tools that default to localhost
|
||||
if tool_type and tool_type.interface_type == "web":
|
||||
_ensure_web_bind_address(instance.compose_path, tool_type.name)
|
||||
|
||||
# Execute docker compose up with env file
|
||||
logger.debug(
|
||||
"Running docker compose up for instance %s (compose_path=%s)",
|
||||
|
||||
@@ -384,9 +384,7 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int:
|
||||
raise RuntimeError(f"No free port found in range {start}-{end}")
|
||||
|
||||
|
||||
def _check_app_binding(
|
||||
container_name: str, port: int
|
||||
) -> dict[str, str | bool]:
|
||||
def _check_app_binding(container_name: str, port: int) -> dict[str, str | bool]:
|
||||
"""Diagnose whether the app is bound to 127.0.0.1 or 0.0.0.0.
|
||||
|
||||
Checks from both inside the container (localhost) and outside
|
||||
|
||||
Reference in New Issue
Block a user