Fix: recognise a stack's own running ports on re-deploy (0.21.2)

Port-conflict check now matches the stack's own containers via the
compose project label instead of a fragile container-name prefix, so
editing + deploying a running stack no longer reports false conflicts
(explicit container_name or '_' name separator).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-08 16:31:52 +00:00
co-authored by Claude Opus 4.8
parent bb29ef1c98
commit f65ec5f268
2 changed files with 22 additions and 7 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ async def lifespan(app: FastAPI):
schedule_task.cancel()
app = FastAPI(title="StackPilot", version="0.21.1", lifespan=lifespan)
app = FastAPI(title="StackPilot", version="0.21.2", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
+21 -6
View File
@@ -103,12 +103,21 @@ def host_listening_ports() -> dict[str, set[int]]:
}
def docker_bound_ports() -> dict[tuple[int, str], str]:
"""Return {(host_port, proto): container_name}."""
out: dict[tuple[int, str], str] = {}
COMPOSE_PROJECT_LABEL = "com.docker.compose.project"
def docker_bound_ports() -> dict[tuple[int, str], tuple[str, Optional[str]]]:
"""Return {(host_port, proto): (container_name, compose_project)}.
The compose project is taken from the container's compose label so a
stack's own running containers can be reliably recognised on re-deploy,
regardless of explicit container_name or name separator differences.
"""
out: dict[tuple[int, str], tuple[str, Optional[str]]] = {}
try:
client = get_client()
for c in safe_call(client.containers.list):
project = (c.labels or {}).get(COMPOSE_PROJECT_LABEL)
bindings = (c.attrs.get("NetworkSettings") or {}).get("Ports") or {}
for container_port, hosts in bindings.items():
if not hosts:
@@ -117,7 +126,7 @@ def docker_bound_ports() -> dict[tuple[int, str], str]:
for h in hosts:
hp = h.get("HostPort")
if hp:
out[(int(hp), proto)] = c.name
out[(int(hp), proto)] = (c.name, project)
except (DockerError, ValueError):
pass
return out
@@ -140,10 +149,16 @@ def detect_conflicts(yaml_str: str, ignore_stack: Optional[str] = None) -> list[
used_by = None
owner = docker_ports.get((port, proto))
if owner:
owner_name, owner_project = owner
# A container from the same stack (re-deploy) is not a conflict.
if ignore_stack and owner.startswith(f"{ignore_stack}-"):
# Match on the compose project label, falling back to the legacy
# name-prefix check for containers without the label.
if ignore_stack and (
owner_project == ignore_stack
or owner_name.startswith(f"{ignore_stack}-")
):
continue
used_by = f"container {owner}"
used_by = f"container {owner_name}"
elif port in host_ports.get(proto, set()):
used_by = "host process"
if used_by: