Remove networks on compose down
Release rolling / release (push) Has been cancelled

This commit is contained in:
Raven Scott
2026-07-16 04:40:50 -04:00
parent 2c35f5cb82
commit 67dffcadb8
2 changed files with 169 additions and 4 deletions
+67 -4
View File
@@ -570,24 +570,75 @@ export async function listStacks(docker) {
}
}
/** Built-in Docker networks that must never be removed. */
const PREDEFINED_NETWORKS = new Set(['bridge', 'host', 'none'])
/**
* Networks owned by a compose project (created via `docker compose up`).
* @param {import('dockerode')} docker
* @param {string} stackName
*/
async function listProjectNetworks(docker, stackName) {
const networks = await docker.listNetworks()
return networks.filter((n) => {
if (!n?.Name || PREDEFINED_NETWORKS.has(n.Name)) return false
const labels = n.Labels || {}
return labels['com.docker.compose.project'] === stackName
})
}
/**
* Remove compose project networks after containers are gone.
* Matches `docker compose down` behavior (networks yes, volumes no).
* @param {import('dockerode')} docker
* @param {string} stackName
* @returns {Promise<Array<{ id: string, name: string, success: boolean, error?: string }>>}
*/
async function removeProjectNetworks(docker, stackName) {
const projectNetworks = await listProjectNetworks(docker, stackName)
const results = []
for (const netInfo of projectNetworks) {
const id = netInfo.Id
const name = netInfo.Name
try {
await docker.getNetwork(id).remove()
results.push({ id, name, success: true })
logger.info('Removed stack network', { stackName, name, id: id?.slice?.(0, 12) })
} catch (error) {
results.push({ id, name, success: false, error: error.message })
logger.warn('Failed to remove stack network', {
stackName,
name,
error: error.message,
})
}
}
return results
}
/**
* @param {import('dockerode')} docker
* @param {string} stackName
*/
export async function removeComposeStack(docker, stackName) {
// Try compose down if we can find any container with project label (CLI needs a file — use stub)
// Remove project containers, then project-scoped networks (compose down parity).
// CLI `compose down` needs a compose file; we use label-based dockerode cleanup instead.
const containers = await docker.listContainers({ all: true })
const stackContainers = containers.filter((c) => {
const labels = c.Labels || {}
return labels['com.docker.compose.project'] === stackName
})
if (stackContainers.length === 0) {
const networkCandidates = await listProjectNetworks(docker, stackName)
if (stackContainers.length === 0 && networkCandidates.length === 0) {
return {
success: true,
stackName,
removed: 0,
networksRemoved: 0,
results: [],
networkResults: [],
message: `Stack "${stackName}" not found or already removed`,
}
}
@@ -606,12 +657,24 @@ export async function removeComposeStack(docker, stackName) {
}
}
// Containers must be gone (or disconnected) before networks can be removed.
const networkResults = await removeProjectNetworks(docker, stackName)
const removed = results.filter((r) => r.success).length
const networksRemoved = networkResults.filter((r) => r.success).length
const parts = []
if (removed) parts.push(`${removed} container(s)`)
if (networksRemoved) parts.push(`${networksRemoved} network(s)`)
const detail = parts.length ? parts.join(', ') : 'nothing left to remove'
return {
success: true,
stackName,
removed: results.filter((r) => r.success).length,
removed,
networksRemoved,
results,
message: `Stack "${stackName}" removed successfully`,
networkResults,
message: `Stack "${stackName}" removed successfully (${detail})`,
}
}