fix: inject backend network into compose file instead of docker network connect

The post-creation 'docker network connect' was failing silently for
unknown reasons (race condition, container state, or Docker internals).

Instead of fighting with this, we now inject the backend network directly
into the compose file before 'docker compose up'. Docker Compose then
attaches the container to the network atomically during creation.

- apps/api/src/api/tool_instances.py: new _ensure_backend_network_in_compose()
  adds 'networks: [backend_name]' to the service and declares the network
  as external at the top level
- apps/api/src/api/tool_instances.py: call _ensure_backend_network_in_compose()
  in both start_instance and restart_instance, right after
  _ensure_container_name_in_compose()
- apps/api/src/api/tool_instances.py: removed connect_container_to_network
  import and call entirely
- apps/api/src/api/tool_instances.py: import get_backend_network_name from
  docker module for use in the new helper

Quality gates: ruff clean
This commit is contained in:
2026-05-30 14:12:22 +02:00
parent cc52811522
commit 2e156fc534
+46 -15
View File
@@ -45,10 +45,10 @@ 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_logs,
get_container_status,
@@ -750,6 +750,49 @@ def _ensure_web_bind_address(
return
def _ensure_backend_network_in_compose(compose_path: str) -> None:
"""Inject the backend network into the compose file so compose up attaches it.
Instead of running 'docker network connect' after container creation (which
is prone to race conditions and silent failures), we declare the network in
the compose file itself. Docker Compose then connects the container to the
network atomically during 'docker compose up'.
"""
import yaml
from pathlib import Path
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
network_name = get_backend_network_name()
modified = False
for svc_config in compose_data["services"].values():
existing = svc_config.get("networks", [])
if network_name not in existing:
svc_config["networks"] = existing + [network_name]
modified = True
break # Only modify first service
# Declare the network as external at the top level
if "networks" not in compose_data:
compose_data["networks"] = {}
if network_name not in compose_data["networks"]:
compose_data["networks"][network_name] = {"external": True}
modified = True
if modified:
compose_file.write_text(yaml.dump(compose_data, default_flow_style=False))
logger.info("Injected backend network '%s' into compose file", network_name)
@router.post(
"/{project_id}/repositories/{repo_id}/instances",
summary="Create tool instance",
@@ -1690,6 +1733,7 @@ async def start_instance(
# Ensure predictable container name for tunnel connectivity
_ensure_container_name_in_compose(instance.compose_path, instance.name)
_ensure_backend_network_in_compose(instance.compose_path)
# Execute docker compose up with env file
logger.debug(
@@ -1729,20 +1773,6 @@ async def start_instance(
"Container name for instance %s: %s", instance.id, expected_container_name
)
# Connect container to backend network so API can reach it
logger.debug(
"Connecting container %s to backend network...", expected_container_name
)
connected = connect_container_to_network(expected_container_name)
if connected:
logger.debug(
"Successfully connected %s to backend network", expected_container_name
)
else:
logger.warning(
"Failed to connect %s to backend network", expected_container_name
)
# Verify container reached running state
if instance.container_id:
instance.status = "starting"
@@ -2174,6 +2204,7 @@ async def restart_instance(
instance.compose_path, tool_type.name, tool_type.default_port
)
_ensure_container_name_in_compose(instance.compose_path, instance.name)
_ensure_backend_network_in_compose(instance.compose_path)
returncode, stdout, stderr = execute_compose_command(
instance.compose_path, "restart"