fix(cloudflared): use --bind-addr with port for code-server bind fix

Root cause: _ensure_web_bind_address injected --host 0.0.0.0 for code-server,
which only sets the bind host, not the port. code-server then listens on its
default port (8080) instead of the tool type's default_port (8443). Cloudflared
connects to port 8443 and gets connection refused, resulting in a 502.

Changes:
- _ensure_web_bind_address now accepts default_port and builds
  --bind-addr 0.0.0.0:{port} for code-server
- Same fix for jupyter-notebook with explicit --port flag
- Existing broken --host commands are now detected and replaced
- New migration fixes tool_types templates and instance compose files on disk
- Test fixture updated to use correct --bind-addr 0.0.0.0:8443
This commit is contained in:
Developer
2026-05-29 16:49:52 +00:00
parent aa25852091
commit 6bd814e346
3 changed files with 173 additions and 12 deletions
@@ -0,0 +1,148 @@
"""Fix code-server bind address to include port
Revision ID: 2026_05_29_fix_code_server_bind_addr_port
Revises: 2026_05_29_remove_lsio_command_override
Create Date: 2026-05-29 18:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import yaml
# revision identifiers, used by Alembic.
revision: str = "2026_05_29_fix_code_server_bind_addr_port"
down_revision: Union[str, None] = "2026_05_29_remove_lsio_command_override"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _fix_tool_type_templates(conn) -> None:
"""Fix code-server tool type templates with broken --host override."""
result = conn.execute(
sa.text("""
SELECT id, compose_template, default_port
FROM tool_types
WHERE name = 'code-server'
AND compose_template LIKE '%--host%'
""")
).fetchall()
for tool_id, compose_template, default_port in result:
port = default_port or 8443
expected = f"--bind-addr 0.0.0.0:{port}"
# Replace any line containing --host with the correct bind-addr
lines = compose_template.split("\n")
new_lines = []
modified = False
for line in lines:
if "command:" in line and "--host" in line:
indent = line[: len(line) - len(line.lstrip())]
new_lines.append(f"{indent}command: {expected}")
modified = True
else:
new_lines.append(line)
if not modified:
continue
updated = "\n".join(new_lines)
conn.execute(
sa.text("""
UPDATE tool_types
SET compose_template = :compose_template
WHERE id = :id
"""),
{"compose_template": updated, "id": tool_id},
)
print(f"Fixed code-server template ({tool_id}): replaced --host with {expected}")
def _fix_instance_compose_files(conn) -> None:
"""Fix existing instance compose files on disk with broken --host override."""
from pathlib import Path
# Use information_schema to check if compose_path column exists
col_result = conn.execute(
sa.text("""
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'tool_instances'
AND column_name = 'compose_path'
""")
).fetchone()
if not col_result:
print("compose_path column not found, skipping instance file fixes")
return
result = conn.execute(
sa.text("""
SELECT id, compose_path, tool_type_id
FROM tool_instances
WHERE compose_path IS NOT NULL
""")
).fetchall()
for instance_id, compose_path, tool_type_id in result:
path = Path(compose_path)
if not path.exists():
continue
try:
content = path.read_text()
except Exception:
continue
if "--host" not in content:
continue
# Get default_port from tool_type
port_result = conn.execute(
sa.text("""
SELECT default_port FROM tool_types WHERE id = :id
"""),
{"id": tool_type_id},
).fetchone()
port = port_result[0] if port_result and port_result[0] else 8443
expected = f"--bind-addr 0.0.0.0:{port}"
try:
data = yaml.safe_load(content)
except Exception:
continue
if not data or "services" not in data:
continue
modified = False
for svc in data["services"].values():
if "command" in svc:
cmd = svc["command"]
if "--host" in cmd:
svc["command"] = expected
modified = True
if not modified:
continue
try:
path.write_text(yaml.dump(data, default_flow_style=False))
print(
f"Fixed code-server instance compose ({instance_id}): "
f"replaced --host with {expected}"
)
except Exception as exc:
print(f"Failed to fix instance {instance_id}: {exc}")
def upgrade() -> None:
conn = op.get_bind()
_fix_tool_type_templates(conn)
_fix_instance_compose_files(conn)
def downgrade() -> None:
pass
+24 -11
View File
@@ -652,7 +652,9 @@ def _ensure_container_name_in_compose(compose_path: str, container_name: str) ->
)
def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None:
def _ensure_web_bind_address(
compose_path: str, tool_type_name: str, default_port: int
) -> 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,
@@ -662,9 +664,12 @@ def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None:
import yaml
from pathlib import Path
KNOWN_BIND_FIXES = {
"code-server": "--host 0.0.0.0",
"jupyter-notebook": "start-notebook.sh --ip=0.0.0.0",
if default_port <= 0:
return
KNOWN_BIND_FIXES: dict[str, str] = {
"code-server": f"--bind-addr 0.0.0.0:{default_port}",
"jupyter-notebook": f"start-notebook.sh --ip=0.0.0.0 --port={default_port} --no-browser",
}
bind_command = KNOWN_BIND_FIXES.get(tool_type_name)
@@ -713,8 +718,15 @@ def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None:
existing_command = service_config.get("command", "")
if existing_command:
# Fix broken --bind-addr (replaces with --host)
if "--bind-addr" in existing_command:
# Already correct — nothing to do
if bind_command in existing_command:
return
# Fix broken or outdated bind flags
if (
"--bind-addr" in existing_command
or "--host" in existing_command
or "--ip=" in existing_command
):
service_config["command"] = bind_command
compose_file.write_text(
yaml.dump(compose_data, default_flow_style=False)
@@ -726,9 +738,6 @@ def _ensure_web_bind_address(compose_path: str, tool_type_name: str) -> None:
bind_command,
)
return
# Already has correct --host, nothing to do
if "--host" in existing_command or "--ip=" in existing_command:
return
# Some other command override exists — don't touch it
return
@@ -1673,7 +1682,9 @@ async def start_instance(
# 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)
_ensure_web_bind_address(
instance.compose_path, tool_type.name, tool_type.default_port
)
# Ensure predictable container name for tunnel connectivity
_ensure_container_name_in_compose(instance.compose_path, instance.name)
@@ -2151,7 +2162,9 @@ async def restart_instance(
_sanitize_compose_file(instance.compose_path)
tool_type = await session.get(ToolType, instance.tool_type_id)
if tool_type and tool_type.interface_type == "web":
_ensure_web_bind_address(instance.compose_path, tool_type.name)
_ensure_web_bind_address(
instance.compose_path, tool_type.name, tool_type.default_port
)
_ensure_container_name_in_compose(instance.compose_path, instance.name)
returncode, stdout, stderr = execute_compose_command(
@@ -184,7 +184,7 @@ class TestToolTypesAPIExtended:
"interfaces": ["web", "terminal"],
"default_port": 8443,
"definition_type": "compose",
"compose_template": "version: '3.8'\nservices:\n app:\n image: code-server\n command: --host 0.0.0.0\n ports:\n - '8443:8443'\n volumes:\n - \"{{REPO_PATH}}:/workspace\"",
"compose_template": "version: '3.8'\nservices:\n app:\n image: code-server\n command: --bind-addr 0.0.0.0:8443\n ports:\n - '8443:8443'\n volumes:\n - \"{{REPO_PATH}}:/workspace\"",
"readiness_probe": {
"command": "curl -f http://localhost:8443",
"timeout": 30,