diff --git a/api/app.py b/api/app.py index c23eeb7..d278f38 100644 --- a/api/app.py +++ b/api/app.py @@ -508,8 +508,12 @@ def update_config(): # Write new .env with updated IPs (and current ports) for next container start env_file = os.environ.get('COMPOSE_ENV_FILE', '/app/.env.compose') ip_utils.write_env_file(new_range, env_file, _collect_service_ports(config_manager.configs)) - # Mark ALL containers as needing restart (IPs affect every container) - _set_pending_restart([f'ip_range changed to {new_range} — container IPs updated'], ['*']) + # Mark ALL containers as needing restart; network_recreate signals that + # docker compose down is required before up (Docker can't change subnet in-place) + _set_pending_restart( + [f'ip_range changed to {new_range} — network will be recreated'], + ['*'], network_recreate=True + ) # Detect port changes across service configs and identity # Maps (service_key, field_name) → (port_env_key, [containers]) @@ -595,10 +599,11 @@ def _collect_service_ports(configs: dict) -> dict: return ports -def _set_pending_restart(changes: list, containers: list = None): +def _set_pending_restart(changes: list, containers: list = None, network_recreate: bool = False): """Record that specific containers need to be restarted to apply configuration. containers: list of docker-compose service names, or None/'*' to restart all. + network_recreate: True when the Docker bridge subnet changed (requires down+up). Merges with any existing pending state so multiple changes accumulate. """ from datetime import datetime as _dt @@ -616,13 +621,14 @@ def _set_pending_restart(changes: list, containers: list = None): 'changed_at': _dt.utcnow().isoformat(), 'changes': existing_changes + changes, 'containers': new_containers, + 'network_recreate': network_recreate or existing.get('network_recreate', False), } config_manager._save_all_configs() def _clear_pending_restart(): config_manager.configs['_pending_restart'] = { - 'needs_restart': False, 'changes': [], 'containers': [] + 'needs_restart': False, 'changes': [], 'containers': [], 'network_recreate': False } config_manager._save_all_configs() @@ -669,24 +675,39 @@ def apply_pending_config(): # Clear pending flag before we restart so it shows cleared after new containers start _clear_pending_restart() - # Build compose args: restart all, or only the specific changed containers - if '*' in containers: - compose_up_args = ['up', '-d'] - else: - compose_up_args = ['up', '-d', '--no-deps'] + containers + # Check if the IP range (network subnet) is changing — Docker cannot modify an + # existing network's subnet in-place, so we need `down` + `up` in that case. + needs_network_recreate = pending.get('network_recreate', False) - # Run docker compose up -d in a background thread; the 0.3s delay lets Flask - # finish sending this response before cell-api itself gets recreated. + if '*' in containers: + if needs_network_recreate: + # down removes containers AND the bridge network; up recreates everything + compose_down_args = ['down'] + compose_up_args = ['up', '-d'] + else: + compose_down_args = None + compose_up_args = ['up', '-d'] + else: + compose_down_args = None + compose_up_args = ['up', '-d', '--no-deps'] + containers + + base_cmd = ['docker', 'compose', + '--project-directory', project_dir, + '-f', '/app/docker-compose.yml', + '--env-file', '/app/.env.compose'] + + # Run in a background thread; 0.3 s delay lets Flask send this response first. def _do_apply(): import time as _time _time.sleep(0.3) - result = subprocess.run( - ['docker', 'compose', - '--project-directory', project_dir, - '-f', '/app/docker-compose.yml', - '--env-file', '/app/.env.compose'] + compose_up_args, - capture_output=True, text=True, timeout=120 - ) + if compose_down_args: + r = subprocess.run(base_cmd + compose_down_args, + capture_output=True, text=True, timeout=60) + if r.returncode != 0: + logger.error(f"docker compose down failed: {r.stderr.strip()}") + return + result = subprocess.run(base_cmd + compose_up_args, + capture_output=True, text=True, timeout=120) if result.returncode != 0: logger.error(f"docker compose up failed: {result.stderr.strip()}") else: