This commit is contained in:
jarek committed 2026-02-16 08:46:56 +01:00
1 parent d83ca684d7
commit 8ee4fe4d68
49 files changed
+2126 -261

No files matched your search

+58 -3
View File
@@ -1562,6 +1562,43 @@ export async function getStackPathHints(
return { workingDir, configFiles };
}
/**
* Stop or remove orphan containers that belong to a stack but aren't defined in the compose file.
* These are dynamically-spawned child containers (e.g., nextcloud-aio master creates worker containers).
* Best-effort: errors are logged but don't fail the overall operation.
*/
async function cleanupOrphanStackContainers(
stackName: string,
envId: number | null | undefined,
operation: 'stop' | 'remove' | 'restart'
): Promise<void> {
try {
const containers = await getStackContainers(stackName, envId);
const targets = containers.filter(
(c) => c.state === 'running' || c.state === 'restarting'
);
if (targets.length === 0) return;
const { stopContainer, removeContainer, restartContainer } = await import('./docker.js');
const results = await Promise.allSettled(
targets.map((c) => {
if (operation === 'remove') return removeContainer(c.id, true, envId);
if (operation === 'restart') return restartContainer(c.id, envId);
return stopContainer(c.id, envId);
})
);
const failures = results.filter((r) => r.status === 'rejected');
if (failures.length > 0) {
console.warn(
`[stacks] ${failures.length} orphan container(s) failed to ${operation} for stack "${stackName}"`
);
}
} catch (err) {
console.warn(`[stacks] Failed to cleanup orphan containers for stack "${stackName}":`, err);
}
}
/**
* Helper to perform container-based operations for external stacks
* Used as fallback when no compose file exists.
@@ -1767,13 +1804,18 @@ export async function stopStack(
return withContainerFallback(stackName, envId, 'stop');
}
return executeComposeCommand(
const composeResult = await executeComposeCommand(
'stop',
{ stackName, envId, workingDir: result.stackDir, composePath: result.composePath, envPath: result.envPath },
result.content!,
result.nonSecretVars,
result.secretVars
);
// Stop any dynamically-spawned child containers not in the compose file
await cleanupOrphanStackContainers(stackName, envId, 'stop');
return composeResult;
}
/**
@@ -1791,13 +1833,18 @@ export async function restartStack(
return withContainerFallback(stackName, envId, 'restart');
}
return executeComposeCommand(
const composeResult = await executeComposeCommand(
'restart',
{ stackName, envId, workingDir: result.stackDir, composePath: result.composePath, envPath: result.envPath },
result.content!,
result.nonSecretVars,
result.secretVars
);
// Restart any dynamically-spawned child containers not in the compose file
await cleanupOrphanStackContainers(stackName, envId, 'restart');
return composeResult;
}
/**
@@ -1816,13 +1863,18 @@ export async function downStack(
return withContainerFallback(stackName, envId, 'stop');
}
return executeComposeCommand(
const composeResult = await executeComposeCommand(
'down',
{ stackName, envId, removeVolumes, workingDir: result.stackDir, composePath: result.composePath, envPath: result.envPath },
result.content!,
result.nonSecretVars,
result.secretVars
);
// Remove any dynamically-spawned child containers not in the compose file
await cleanupOrphanStackContainers(stackName, envId, 'remove');
return composeResult;
}
/**
@@ -1861,6 +1913,9 @@ export async function removeStack(
if (!downResult.success && !force) {
return downResult;
}
// Remove any dynamically-spawned child containers not handled by compose
await cleanupOrphanStackContainers(stackName, envId, 'remove');
} else {
// External stack - remove containers directly in parallel
const { removeContainer } = await import('./docker.js');