From cb9221113d4b88a44133b077cd646013dd035c69 Mon Sep 17 00:00:00 2001 From: Chris Sherwood Date: Fri, 5 Jun 2026 16:24:32 -0700 Subject: [PATCH] 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) --- admin/app/services/docker_service.ts | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/admin/app/services/docker_service.ts b/admin/app/services/docker_service.ts index 69f3ac5..87a7742 100644 --- a/admin/app/services/docker_service.ts +++ b/admin/app/services/docker_service.ts @@ -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}`) } }