fix(system): show a clear message when a service port is already in use

When a service fails to install because something on the host already binds its
port, the user saw the raw dockerode error ("Bind for 0.0.0.0:11434 failed:
port is already allocated"), which is meaningless to a non-technical user. The
most common case is a native Ollama install holding 11434.

Add _humanizeDockerError() to map host port-conflict errors to an actionable
message that names the port and, for Ollama/11434, points at the likely cause
(a host Ollama service) with the commands to stop it. Unrecognized errors pass
through unchanged. Wired into the install failure broadcast and the thrown
error.

Closes #934

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-06-05 16:24:32 -07:00 committed by jakeaturner
parent cf8db6218d
commit 3f574e4003
No known key found for this signature in database
GPG Key ID: B1072EBDEECE328D
1 changed files with 27 additions and 2 deletions

View File

@ -462,6 +462,30 @@ export class DockerService {
* @param serviceName
* @returns
*/
/**
* Translate low-level dockerode errors into something a non-technical user can
* act on. Currently handles host port conflicts the most common install
* failure, where a service can't bind its port because something on the host
* already holds it (classic case: a native Ollama install owns 11434). Returns
* the original message unchanged for anything we don't recognize. (#934)
*/
private _humanizeDockerError(error: any, serviceName: string): string {
const raw: string = error?.message ?? String(error)
// dockerode surfaces port conflicts as e.g.
// "...Bind for 0.0.0.0:11434 failed: port is already allocated"
// "...listen tcp 0.0.0.0:8090: bind: address already in use"
const portMatch = raw.match(/(?:Bind for [^:]+:(\d+) failed: port is already allocated|:(\d+): bind: address already in use)/i)
if (portMatch) {
const port = portMatch[1] || portMatch[2]
const portText = port ? `port ${port}` : 'a required port'
if (port === '11434' || serviceName === SERVICE_NAMES.OLLAMA) {
return `Couldn't start because ${portText} is already in use on this machine. This usually means Ollama is already installed and running directly on the host (outside NOMAD). Stop and disable the host Ollama service (e.g. "sudo systemctl stop ollama" then "sudo systemctl disable ollama"), then try again.`
}
return `Couldn't start because ${portText} is already in use on this machine. Stop whatever is using ${portText} on the host, then try again.`
}
return raw
}
/**
* Resolve the host filesystem path that backs the admin container's storage
* directory (`/app/storage`). Child services are created via the Docker socket,
@ -828,14 +852,15 @@ export class DockerService {
`Service ${service.service_name} installation completed successfully.`
)
} catch (error: any) {
const friendly = this._humanizeDockerError(error, service.service_name)
this._broadcast(
service.service_name,
'error',
`Error installing service ${service.service_name}: ${error.message}`
`Error installing service ${service.service_name}: ${friendly}`
)
// Mark install as failed and cleanup
await this._cleanupFailedInstallation(service.service_name)
throw new Error(`Failed to install service ${service.service_name}: ${error.message}`)
throw new Error(`Failed to install service ${service.service_name}: ${friendly}`)
}
}