Park source container on duplicate so host ports can be reused
Release rolling / release (push) Has been cancelled

When duplicating with the same host ports, stop+rename the origin, deploy
and verify the new container, then remove the parked source. Client
precheck ignores ports owned only by the origin to avoid false blocks.
This commit is contained in:
Raven Scott
2026-07-16 22:24:49 -04:00
parent ca3770da59
commit bd512df950
7 changed files with 520 additions and 28 deletions
+29 -7
View File
@@ -759,12 +759,15 @@ export async function deployContainerWithSteps(args) {
const state = { pulledOk: false }
const replace = args.replace === true
const fromAdd = args.source === 'add-container'
const fromDuplicate = args.source === 'duplicate' || Boolean(args.sourceContainerId)
const alwaysPull = args.alwaysPull !== false
const jobLabel = replace
? `Replace ${args.containerName || 'container'}`
: fromAdd
? `Create ${args.containerName || 'container'}`
: `Deploy ${args.containerName || 'container'}`
: fromDuplicate
? `Duplicate ${args.containerName || 'container'}`
: fromAdd
? `Create ${args.containerName || 'container'}`
: `Deploy ${args.containerName || 'container'}`
return runJob(
jobLabel,
@@ -782,6 +785,11 @@ export async function deployContainerWithSteps(args) {
log(`Image=${args.image}`)
log(`Always pull image: ${alwaysPull ? 'yes' : 'no (local if present)'}`)
if (replace) log('Mode: replace existing container with same name')
if (args.sourceContainerId) {
log(
`Origin: ${args.sourceContainerName || String(args.sourceContainerId).slice(0, 12)} — will stop+rename if name/ports conflict, remove after new container is verified`
)
}
if (args.publishAllPorts) log('Publish all exposed ports: yes')
if (args.ports?.length) log(`Ports: ${args.ports.join(', ')}`)
if (args.volumes?.length) log(`Volumes: ${args.volumes.join(', ')}`)
@@ -845,12 +853,18 @@ export async function deployContainerWithSteps(args) {
},
{
id: 'create',
label: replace ? 'Replace, create & start' : 'Create & start container',
label: replace
? 'Replace, create & start'
: fromDuplicate
? 'Swap origin, create & verify'
: 'Create & start container',
run: async ({ log }) => {
log(
replace
? 'Removing previous container (if present), then creating…'
: 'Creating and starting container…'
: args.sourceContainerId
? 'Parking origin if needed (stop+rename), then create/start/verify…'
: 'Creating and starting container…'
)
// Only skip server pull when client pull already succeeded
const payload = {
@@ -863,7 +877,15 @@ export async function deployContainerWithSteps(args) {
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')
if (res?.swapped) {
log(
res.parkedName
? `Origin was parked then removed after verify (was ${res.parkedName})`
: 'Origin was parked then removed after verify'
)
} else if (replace) {
log('Previous container was replaced')
}
return res
} catch (err) {
const info = explainError(err, 'deployContainer')
@@ -891,7 +913,7 @@ export async function deployContainerWithSteps(args) {
],
{
peerId: manager.active?.id,
icon: replace ? 'fa-recycle' : 'fa-rocket',
icon: replace ? 'fa-recycle' : fromDuplicate ? 'fa-clone' : 'fa-rocket',
subtitle: args.image || args.containerName || null,
}
)