Add container create page from the containers list with always-pull.
Release rolling / release (push) Successful in 8m24s

Adds a dedicated blank create form (name, image, always pull, ports,
auto-remove, advanced settings) separate from template Deploy, reuses
deployContainer with pull-if-missing and publish-all-ports support.
This commit is contained in:
Raven Scott
2026-07-15 14:27:05 -04:00
parent bbf0607aaf
commit b185ea7a55
9 changed files with 1055 additions and 24 deletions
+45 -14
View File
@@ -7,6 +7,20 @@ import { broadcastContainers } from './containers.js'
import logger from '../utils/logger.js'
import { formatDeployError } from '../utils/dockerErrors.js'
/**
* @param {string} imageRef
* @returns {Promise<boolean>}
*/
async function imageExistsLocally(imageRef) {
if (!imageRef) return false
try {
await docker.getImage(imageRef).inspect()
return true
} catch {
return false
}
}
export function registerDeployHandlers(session) {
session.respond('deployContainer', async (args) => {
const containerName = validation.sanitizeString(args.containerName, 63)
@@ -33,7 +47,7 @@ export function registerDeployHandlers(session) {
})
)
// Portainer-style replace: stop+remove the name holder, then create new
// Optional replace: stop+remove the name holder, then create new
if (existing) {
if (args.replace === true) {
logger.info('Replacing existing container', {
@@ -74,20 +88,33 @@ export function registerDeployHandlers(session) {
}
}
// Skip pull when client already pulled (job stepper) unless force
// Image pull policy:
// - skipPull: client already pulled successfully — do nothing
// - alwaysPull === false: use local image if present; pull only when missing
// - alwaysPull true/undefined: force pull (legacy deploy / template behavior)
if (!args.skipPull) {
logger.info(`Pulling Docker image: ${args.image}`)
try {
const pullStream = await docker.pull(args.image)
await new Promise((resolve, reject) => {
docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve()))
})
} catch (pullErr) {
throw formatDeployError(pullErr, {
stage: 'pull',
containerName: args.containerName,
image: args.image,
})
const forcePull = args.alwaysPull !== false
let needPull = forcePull
if (!forcePull) {
needPull = !(await imageExistsLocally(args.image))
if (!needPull) {
logger.info('Using local image (alwaysPull=false)', { image: args.image })
}
}
if (needPull) {
logger.info(`Pulling Docker image: ${args.image}`, { force: forcePull })
try {
const pullStream = await docker.pull(args.image)
await new Promise((resolve, reject) => {
docker.modem.followProgress(pullStream, (err) => (err ? reject(err) : resolve()))
})
} catch (pullErr) {
throw formatDeployError(pullErr, {
stage: 'pull',
containerName: args.containerName,
image: args.image,
})
}
}
}
@@ -155,6 +182,10 @@ export function registerDeployHandlers(session) {
NetworkMode: args.networkMode || 'bridge',
}
if (args.publishAllPorts === true) {
hostConfig.PublishAllPorts = true
}
if (args.ports && Array.isArray(args.ports)) {
hostConfig.PortBindings = {}
for (const portStr of args.ports) {