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
+27 -4
View File
@@ -344,9 +344,12 @@ export function isJobDrawerActive() {
export async function deployContainerWithSteps(args) {
/** @type {{ pulledOk: boolean }} */
const state = { pulledOk: false }
const replace = args.replace === true
return runJob(
`Deploy ${args.containerName || 'container'}`,
replace
? `Replace ${args.containerName || 'container'}`
: `Deploy ${args.containerName || 'container'}`,
[
{
id: 'validate',
@@ -359,6 +362,7 @@ export async function deployContainerWithSteps(args) {
}
log(`Name=${args.containerName}`)
log(`Image=${args.image}`)
if (replace) log('Mode: replace existing container with same name')
if (args.ports?.length) log(`Ports: ${args.ports.join(', ')}`)
if (args.volumes?.length) log(`Volumes: ${args.volumes.join(', ')}`)
if (args.networkMode) log(`Network mode: ${args.networkMode}`)
@@ -391,18 +395,37 @@ export async function deployContainerWithSteps(args) {
},
{
id: 'create',
label: 'Create & start container',
label: replace ? 'Replace, create & start' : 'Create & start container',
run: async ({ log }) => {
log('Creating and starting container…')
log(
replace
? 'Removing previous container (if present), then creating…'
: 'Creating and starting container…'
)
// Only skip server pull when client pull already succeeded
const payload = { ...args, skipPull: state.pulledOk === true }
const payload = {
...args,
skipPull: state.pulledOk === true,
replace: replace === true,
}
try {
const res = await manager.request(Methods.deployContainer, payload)
log(res?.message || `Container "${args.containerName}" is running`)
if (res?.id) log(`ID: ${String(res.id).slice(0, 12)}`)
if (replace) log('Previous container was replaced')
return res
} catch (err) {
const info = explainError(err, 'deployContainer')
// Keep conflict code so outer deploy can offer Replace
if (
err?.code === 'CONTAINER_NAME_CONFLICT' ||
info.code === 'CONTAINER_NAME_CONFLICT'
) {
const e = new Error(err.message || info.message)
e.code = 'CONTAINER_NAME_CONFLICT'
e.cause = err
throw e
}
// Re-throw with full descriptive text for the job log formatter
const full = [info.title, info.message, info.recovery ? `How to fix: ${info.recovery}` : '']
.filter(Boolean)