Add Portainer-style container name replace on deploy.
Release rolling / release (push) Has been cancelled

When deploying a container whose name already exists, prompt to Replace
(stop and remove the old container) or Cancel. Server accepts replace:true
to perform the swap; clients pre-check names and retry on conflict races.
This commit is contained in:
Raven Scott
2026-07-13 18:16:54 -04:00
parent b2e04caabc
commit 245d532efd
5 changed files with 244 additions and 29 deletions
+52 -8
View File
@@ -26,13 +26,52 @@ export function registerDeployHandlers(session) {
args.image = image
const existingContainers = await docker.listContainers({ all: true })
if (existingContainers.some((c) => c.Names.includes(`/${args.containerName}`))) {
throw formatDeployError(
new Error(
`Conflict. The container name "/${args.containerName}" is already in use by container. You have to remove (or rename) that container to be able to reuse that name.`
),
{ stage: 'create', containerName: args.containerName, image: args.image }
)
const existing = existingContainers.find((c) =>
(c.Names || []).some((n) => {
const bare = String(n || '').replace(/^\//, '')
return bare === args.containerName || n === `/${args.containerName}`
})
)
// Portainer-style replace: stop+remove the name holder, then create new
if (existing) {
if (args.replace === true) {
logger.info('Replacing existing container', {
name: args.containerName,
id: String(existing.Id || '').slice(0, 12),
state: existing.State,
})
const old = docker.getContainer(existing.Id)
try {
await old.stop({ t: 10 })
} catch {
// already stopped / gone
}
try {
await old.remove({ force: true })
} catch (rmErr) {
throw formatDeployError(rmErr, {
stage: 'replace',
containerName: args.containerName,
image: args.image,
})
}
} else {
const err = new Error(
`A container named "${args.containerName}" already exists` +
(existing.State ? ` (${existing.State})` : '') +
(existing.Image ? ` · image ${existing.Image}` : '') +
'. Deploying will stop and remove it, then create a new container with your settings.'
)
err.code = 'CONTAINER_NAME_CONFLICT'
err.existing = {
id: existing.Id,
name: args.containerName,
state: existing.State || 'unknown',
image: existing.Image || '',
}
throw err
}
}
// Skip pull when client already pulled (job stepper) unless force
@@ -278,13 +317,18 @@ export function registerDeployHandlers(session) {
logger.info('Container deployed successfully', {
name: args.containerName,
image: args.image,
replaced: args.replace === true,
})
await broadcastContainers()
return {
success: true,
message: `Container "${args.containerName}" deployed successfully from image "${args.image}"`,
message:
args.replace === true
? `Container "${args.containerName}" replaced successfully from image "${args.image}"`
: `Container "${args.containerName}" deployed successfully from image "${args.image}"`,
id: container.id,
replaced: args.replace === true,
}
})
}