diff --git a/services/node-agent/index.js b/services/node-agent/index.js index e8c38f3..31fa9d9 100644 --- a/services/node-agent/index.js +++ b/services/node-agent/index.js @@ -733,18 +733,29 @@ async function freeCapturePort(capturePort) { const listRes = await dockerApi('GET', '/containers/json?all=1'); if (listRes.status !== 200 || !Array.isArray(listRes.data)) return; for (const c of listRes.data) { - const img = c.Image || ''; - if (!/wild-dragon-capture/.test(img)) continue; - // Inspect to read the PORT env (list payload doesn't include env). + // NOTE: do NOT pre-filter on c.Image here. After `wild-dragon-capture:latest` + // is rebuilt, the Docker list API reports older containers' .Image as the + // bare image ID (e.g. "226f9c953799") instead of the tag, so a regex on the + // tag silently SKIPS those orphans — they keep holding the host port and the + // replacement sidecar dies with EADDRINUSE ("connecting forever"). Identify + // capture sidecars by their PORT env (+ inspected Config.Image) instead, + // which survives a tag rebuild. try { const insp = await dockerApi('GET', `/containers/${c.Id}/json`); - const cenv = (insp.status === 200 && insp.data?.Config?.Env) || []; + if (insp.status !== 200) continue; + const cfg = insp.data?.Config || {}; + const cenv = cfg.Env || []; const portEnv = cenv.find(e => e.startsWith('PORT=')); const p = portEnv ? parseInt(portEnv.split('=')[1], 10) : NaN; - if (p === capturePort) { - console.log(`[sidecar] force-freeing capture port ${capturePort}: removing stale container ${c.Id.slice(0, 12)}`); - await dockerApi('DELETE', `/containers/${c.Id}?force=true`).catch(() => {}); - } + if (p !== capturePort) continue; + // Config.Image (from inspect) preserves the original "wild-dragon-capture:..." + // string even after a tag rebuild — use it as a sanity guard so we only ever + // remove our own capture sidecars, never an unrelated host-net container that + // happens to expose the same PORT env. + const cfgImg = cfg.Image || ''; + if (!/wild-dragon-capture/.test(cfgImg)) continue; + console.log(`[sidecar] force-freeing capture port ${capturePort}: removing stale container ${c.Id.slice(0, 12)} (image=${cfgImg})`); + await dockerApi('DELETE', `/containers/${c.Id}?force=true`).catch(() => {}); } catch (_) { /* container vanished mid-scan — fine */ } } } catch (e) {