Route Coolify through a scanned Comfy proxy instead of pinning hopped listen ports.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+5
-2
@@ -1,13 +1,16 @@
|
|||||||
PORT=3000
|
PORT=3000
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
# ComfyUI on the Windows desktop. Desktop hops ports (8188/8189/8190/...).
|
# ComfyUI on the Windows desktop hops listen ports (8188/8189/8190/...).
|
||||||
# The host agent (scripts/comfy-host-agent.mjs) finds the live instance and proxies it on 8198.
|
# Coolify never talks to those. The host agent scans for the live instance
|
||||||
|
# and always fronts it on 8198. Point the site at that door (or just set
|
||||||
|
# COMFY_CONTROL_URL — the app rewrites Beast HTTP to 8198 automatically).
|
||||||
COMFY_HOST=http://192.168.77.7:8198
|
COMFY_HOST=http://192.168.77.7:8198
|
||||||
COMFY_PORT=8198
|
COMFY_PORT=8198
|
||||||
# Optional: command used when this server runs on the Comfy host
|
# Optional: command used when this server runs on the Comfy host
|
||||||
COMFY_LAUNCH_CMD=
|
COMFY_LAUNCH_CMD=
|
||||||
# Windows host agent (scripts/comfy-host-agent.mjs) so Coolify can start Comfy remotely
|
# Windows host agent (scripts/comfy-host-agent.mjs) so Coolify can start Comfy remotely
|
||||||
COMFY_CONTROL_URL=http://192.168.77.7:8199
|
COMFY_CONTROL_URL=http://192.168.77.7:8199
|
||||||
|
# COMFY_PROXY_PORT=8198
|
||||||
COMFY_CONTROL_TOKEN=
|
COMFY_CONTROL_TOKEN=
|
||||||
# Optional. If set, MyMonitor must send it as X-Monitor-Token (or ?token=).
|
# Optional. If set, MyMonitor must send it as X-Monitor-Token (or ?token=).
|
||||||
# MONITOR_TOKEN=
|
# MONITOR_TOKEN=
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export default defineNuxtConfig({
|
|||||||
comfyLaunchCmd: process.env.COMFY_LAUNCH_CMD || '',
|
comfyLaunchCmd: process.env.COMFY_LAUNCH_CMD || '',
|
||||||
comfyControlUrl: process.env.COMFY_CONTROL_URL || '',
|
comfyControlUrl: process.env.COMFY_CONTROL_URL || '',
|
||||||
comfyControlToken: process.env.COMFY_CONTROL_TOKEN || '',
|
comfyControlToken: process.env.COMFY_CONTROL_TOKEN || '',
|
||||||
|
comfyProxyPort: process.env.COMFY_PROXY_PORT || '8198',
|
||||||
monitorToken: process.env.MONITOR_TOKEN || '',
|
monitorToken: process.env.MONITOR_TOKEN || '',
|
||||||
imageComfyHost: process.env.IMAGE_COMFY_HOST || '',
|
imageComfyHost: process.env.IMAGE_COMFY_HOST || '',
|
||||||
imageComfyPort: process.env.IMAGE_COMFY_PORT || '',
|
imageComfyPort: process.env.IMAGE_COMFY_PORT || '',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const execFileAsync = promisify(execFile)
|
|||||||
const port = Number(process.env.COMFY_CONTROL_PORT || 8199)
|
const port = Number(process.env.COMFY_CONTROL_PORT || 8199)
|
||||||
const proxyPort = Number(process.env.COMFY_PROXY_PORT || 8198)
|
const proxyPort = Number(process.env.COMFY_PROXY_PORT || 8198)
|
||||||
const token = process.env.COMFY_CONTROL_TOKEN || ''
|
const token = process.env.COMFY_CONTROL_TOKEN || ''
|
||||||
const defaultHttp = (process.env.COMFY_HOST || 'http://127.0.0.1:8188').replace(/\/$/, '')
|
const defaultHttp = (process.env.COMFY_LISTEN || 'http://127.0.0.1:8188').replace(/\/$/, '')
|
||||||
const idleMs = Math.max(60_000, Number(process.env.COMFY_IDLE_MS || 30 * 60 * 1000) || 30 * 60 * 1000)
|
const idleMs = Math.max(60_000, Number(process.env.COMFY_IDLE_MS || 30 * 60 * 1000) || 30 * 60 * 1000)
|
||||||
const trainingUrl = String(process.env.TRAINING_CONTROL_URL || 'http://127.0.0.1:8200').replace(/\/$/, '')
|
const trainingUrl = String(process.env.TRAINING_CONTROL_URL || 'http://127.0.0.1:8200').replace(/\/$/, '')
|
||||||
|
|
||||||
@@ -94,14 +94,20 @@ function candidatePorts() {
|
|||||||
return ordered
|
return ordered
|
||||||
}
|
}
|
||||||
|
|
||||||
async function probeStats(portNum, timeoutMs = 800) {
|
const probeSkipUntil = new Map()
|
||||||
|
|
||||||
|
async function probeStats(portNum, timeoutMs = 400) {
|
||||||
|
if (Date.now() < (probeSkipUntil.get(portNum) || 0)) return null
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`http://127.0.0.1:${portNum}/system_stats`, { signal: AbortSignal.timeout(timeoutMs) })
|
const res = await fetch(`http://127.0.0.1:${portNum}/system_stats`, { signal: AbortSignal.timeout(timeoutMs) })
|
||||||
if (!res.ok) return null
|
if (!res.ok) return null
|
||||||
const stats = await res.json()
|
const stats = await res.json()
|
||||||
if (!stats?.system) return null
|
if (!stats?.system) return null
|
||||||
|
probeSkipUntil.delete(portNum)
|
||||||
return stats
|
return stats
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
const timedOut = error?.name === 'TimeoutError' || error?.name === 'AbortError' || /timeout/i.test(String(error?.message || ''))
|
||||||
|
if (timedOut) probeSkipUntil.set(portNum, Date.now() + 30_000)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,15 +139,15 @@ async function findHealthyPort() {
|
|||||||
let proxyServer = null
|
let proxyServer = null
|
||||||
let proxyTarget = 0
|
let proxyTarget = 0
|
||||||
|
|
||||||
function ensureProxy(targetPort) {
|
function ensureProxyListening() {
|
||||||
if (!targetPort) return
|
|
||||||
if (proxyTarget !== targetPort) {
|
|
||||||
proxyTarget = targetPort
|
|
||||||
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-target', listen: proxyPort, target: proxyTarget }))
|
|
||||||
}
|
|
||||||
if (proxyServer) return
|
if (proxyServer) return
|
||||||
proxyServer = net.createServer((client) => {
|
proxyServer = net.createServer((client) => {
|
||||||
const upstream = net.connect(proxyTarget, '127.0.0.1')
|
const target = proxyTarget
|
||||||
|
if (!target) {
|
||||||
|
client.destroy()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const upstream = net.connect(target, '127.0.0.1')
|
||||||
const fail = () => {
|
const fail = () => {
|
||||||
try { client.destroy() } catch { /* ignore */ }
|
try { client.destroy() } catch { /* ignore */ }
|
||||||
try { upstream.destroy() } catch { /* ignore */ }
|
try { upstream.destroy() } catch { /* ignore */ }
|
||||||
@@ -159,26 +165,35 @@ function ensureProxy(targetPort) {
|
|||||||
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-error', error: String(error.message || error) }))
|
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-error', error: String(error.message || error) }))
|
||||||
})
|
})
|
||||||
proxyServer.listen(proxyPort, '0.0.0.0', () => {
|
proxyServer.listen(proxyPort, '0.0.0.0', () => {
|
||||||
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy', listen: proxyPort, target: proxyTarget }))
|
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy', listen: proxyPort, target: proxyTarget || null }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setProxyTarget(targetPort) {
|
||||||
|
ensureProxyListening()
|
||||||
|
const next = Number(targetPort) || 0
|
||||||
|
if (proxyTarget === next) return
|
||||||
|
proxyTarget = next
|
||||||
|
console.log(JSON.stringify({ src: 'comfy-host-agent', event: 'proxy-target', listen: proxyPort, target: proxyTarget || null }))
|
||||||
|
}
|
||||||
|
|
||||||
function markAsleep() {
|
function markAsleep() {
|
||||||
stoppedByAgent = true
|
stoppedByAgent = true
|
||||||
lastQueueRunning = 0
|
lastQueueRunning = 0
|
||||||
lastHealthyPort = 0
|
lastHealthyPort = 0
|
||||||
lastProcessUp = false
|
lastProcessUp = false
|
||||||
lastLaunchAt = 0
|
lastLaunchAt = 0
|
||||||
proxyTarget = 0
|
setProxyTarget(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncProxy() {
|
async function syncProxy() {
|
||||||
const healthyPort = await findHealthyPort()
|
const healthyPort = await findHealthyPort()
|
||||||
lastHealthyPort = healthyPort || 0
|
lastHealthyPort = healthyPort || 0
|
||||||
if (healthyPort) {
|
if (healthyPort) {
|
||||||
ensureProxy(healthyPort)
|
setProxyTarget(healthyPort)
|
||||||
lastProcessUp = true
|
lastProcessUp = true
|
||||||
} else {
|
} else {
|
||||||
|
setProxyTarget(0)
|
||||||
lastProcessUp = false
|
lastProcessUp = false
|
||||||
}
|
}
|
||||||
return healthyPort
|
return healthyPort
|
||||||
@@ -211,7 +226,9 @@ function readJsonFile(path) {
|
|||||||
function desktopListenPort() {
|
function desktopListenPort() {
|
||||||
try {
|
try {
|
||||||
const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`)
|
const url = new URL(defaultHttp.includes('://') ? defaultHttp : `http://${defaultHttp}`)
|
||||||
return Number(url.port || 8188) || 8188
|
const next = Number(url.port || 8188) || 8188
|
||||||
|
if (next === port || next === proxyPort) return 8188
|
||||||
|
return next
|
||||||
} catch {
|
} catch {
|
||||||
return 8188
|
return 8188
|
||||||
}
|
}
|
||||||
@@ -641,7 +658,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
lastActivityAt: new Date(lastWorkAt).toISOString(),
|
lastActivityAt: new Date(lastWorkAt).toISOString(),
|
||||||
idleMs,
|
idleMs,
|
||||||
port: healthyPort || null,
|
port: healthyPort || null,
|
||||||
proxyPort: healthyPort ? proxyPort : null,
|
proxyPort,
|
||||||
training: lastTraining
|
training: lastTraining
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -661,7 +678,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return json(res, 200, { ok: true, started: false, already: true, asleep: false, port: healthyPort, proxyPort })
|
return json(res, 200, { ok: true, started: false, already: true, asleep: false, port: healthyPort, proxyPort })
|
||||||
}
|
}
|
||||||
if (lastLaunchAt && Date.now() - lastLaunchAt < 60_000) {
|
if (lastLaunchAt && Date.now() - lastLaunchAt < 60_000) {
|
||||||
return json(res, 200, { ok: true, started: false, already: true, booting: true, asleep: false })
|
return json(res, 200, { ok: true, started: false, already: true, booting: true, asleep: false, proxyPort })
|
||||||
}
|
}
|
||||||
await taskkillImage('Comfy Desktop.exe')
|
await taskkillImage('Comfy Desktop.exe')
|
||||||
stoppedByAgent = false
|
stoppedByAgent = false
|
||||||
@@ -669,9 +686,9 @@ const server = http.createServer(async (req, res) => {
|
|||||||
const launched = await startComfy()
|
const launched = await startComfy()
|
||||||
if (launched.port) {
|
if (launched.port) {
|
||||||
lastProcessUp = true
|
lastProcessUp = true
|
||||||
ensureProxy(launched.port)
|
setProxyTarget(launched.port)
|
||||||
}
|
}
|
||||||
return json(res, 200, { ok: true, asleep: false, ...launched })
|
return json(res, 200, { ok: true, asleep: false, proxyPort, ...launched })
|
||||||
}
|
}
|
||||||
if (req.method === 'POST' && url.pathname === '/stop') {
|
if (req.method === 'POST' && url.pathname === '/stop') {
|
||||||
const healthyPort = await syncProxy()
|
const healthyPort = await syncProxy()
|
||||||
@@ -697,6 +714,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
server.listen(port, '0.0.0.0', async () => {
|
server.listen(port, '0.0.0.0', async () => {
|
||||||
|
ensureProxyListening()
|
||||||
lastHealthyPort = await syncProxy() || 0
|
lastHealthyPort = await syncProxy() || 0
|
||||||
lastProcessUp = Boolean(lastHealthyPort) || await processUp().catch(() => false)
|
lastProcessUp = Boolean(lastHealthyPort) || await processUp().catch(() => false)
|
||||||
if (lastHealthyPort) await noteQueue(lastHealthyPort)
|
if (lastHealthyPort) await noteQueue(lastHealthyPort)
|
||||||
|
|||||||
+52
-11
@@ -7,18 +7,58 @@ export function setComfyHostOverride(url: string) {
|
|||||||
comfyHostOverride = String(url || '').replace(/\/$/, '')
|
comfyHostOverride = String(url || '').replace(/\/$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function controlUrl() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
return String(config.comfyControlUrl || process.env.COMFY_CONTROL_URL || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function agentFrontDoorOrigin() {
|
||||||
|
const raw = controlUrl()
|
||||||
|
if (!raw) return ''
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const proxyPort = String(config.comfyProxyPort || process.env.COMFY_PROXY_PORT || '8198').trim() || '8198'
|
||||||
|
try {
|
||||||
|
const control = new URL(raw.includes('://') ? raw : `http://${raw}`)
|
||||||
|
control.port = proxyPort
|
||||||
|
control.pathname = ''
|
||||||
|
control.search = ''
|
||||||
|
control.hash = ''
|
||||||
|
return control.origin
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Beast HTTP goes through the host-agent proxy so hopped 8188/8189/8190 never reach Coolify. */
|
||||||
|
export function viaAgentFrontDoor(host: string) {
|
||||||
|
const door = agentFrontDoorOrigin()
|
||||||
|
if (!door || !host) return host
|
||||||
|
try {
|
||||||
|
const url = new URL(host)
|
||||||
|
const front = new URL(door)
|
||||||
|
if (url.hostname !== front.hostname) return host
|
||||||
|
return front.origin
|
||||||
|
} catch {
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function comfyConfigured() {
|
export function comfyConfigured() {
|
||||||
if (comfyHostOverride) return true
|
if (comfyHostOverride) return true
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
return Boolean(String(config.comfyHost || process.env.COMFY_HOST || '').trim())
|
return Boolean(
|
||||||
|
String(config.comfyHost || process.env.COMFY_HOST || '').trim()
|
||||||
|
|| controlUrl()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function comfyBase() {
|
function configuredComfyBase() {
|
||||||
if (comfyHostOverride) return comfyHostOverride
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
let host = String(process.env.COMFY_HOST || config.comfyHost || '').trim().replace(/\/$/, '')
|
let host = String(process.env.COMFY_HOST || config.comfyHost || '').trim().replace(/\/$/, '')
|
||||||
const port = String(process.env.COMFY_PORT || config.comfyPort || '').trim()
|
const port = String(process.env.COMFY_PORT || config.comfyPort || '').trim()
|
||||||
|
const door = agentFrontDoorOrigin()
|
||||||
if (!host) {
|
if (!host) {
|
||||||
|
if (door) return door
|
||||||
throw createError({ statusCode: 500, statusMessage: 'COMFY_HOST is not configured' })
|
throw createError({ statusCode: 500, statusMessage: 'COMFY_HOST is not configured' })
|
||||||
}
|
}
|
||||||
if (!/^https?:\/\//i.test(host)) host = `http://${host}`
|
if (!/^https?:\/\//i.test(host)) host = `http://${host}`
|
||||||
@@ -32,21 +72,22 @@ function comfyBase() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getComfyHost() {
|
export function getComfyHost() {
|
||||||
return comfyBase()
|
if (comfyHostOverride) return viaAgentFrontDoor(comfyHostOverride) || comfyHostOverride
|
||||||
|
return viaAgentFrontDoor(configuredComfyBase()) || configuredComfyBase()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function comfyWsUrl(clientId: string) {
|
export function comfyWsUrl(clientId: string) {
|
||||||
return `${comfyBase().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}`
|
return `${getComfyHost().replace(/^http/, 'ws')}/ws?clientId=${encodeURIComponent(clientId)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function comfyFetch(path: string, init?: RequestInit) {
|
export async function comfyFetch(path: string, init?: RequestInit) {
|
||||||
const url = `${comfyBase()}${path}`
|
const url = `${getComfyHost()}${path}`
|
||||||
try {
|
try {
|
||||||
return await fetch(url, init)
|
return await fetch(url, init)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 502,
|
statusCode: 502,
|
||||||
statusMessage: `ComfyUI host unreachable (${comfyBase()})`,
|
statusMessage: `ComfyUI host unreachable (${getComfyHost()})`,
|
||||||
data: { cause: error instanceof Error ? error.message : String(error) }
|
data: { cause: error instanceof Error ? error.message : String(error) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -306,12 +347,12 @@ export function inspectHistory(history: Record<string, unknown> | null, promptId
|
|||||||
|
|
||||||
export async function probeComfy() {
|
export async function probeComfy() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${comfyBase()}/system_stats`, { signal: AbortSignal.timeout(2500) })
|
const res = await fetch(`${getComfyHost()}/system_stats`, { signal: AbortSignal.timeout(2500) })
|
||||||
if (!res.ok) return { ok: false, host: comfyBase() }
|
if (!res.ok) return { ok: false, host: getComfyHost() }
|
||||||
const stats = await res.json()
|
const stats = await res.json()
|
||||||
return { ok: true, host: comfyBase(), stats }
|
return { ok: true, host: getComfyHost(), stats }
|
||||||
} catch {
|
} catch {
|
||||||
return { ok: false, host: comfyBase() }
|
return { ok: false, host: getComfyHost() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||||
import { getComfyHost } from '~/server/utils/comfy'
|
import { getComfyHost, viaAgentFrontDoor } from '~/server/utils/comfy'
|
||||||
import { comfyJobPrefix } from '~/utils/outputNames'
|
import { comfyJobPrefix } from '~/utils/outputNames'
|
||||||
|
|
||||||
const imageHostAls = new AsyncLocalStorage<string>()
|
const imageHostAls = new AsyncLocalStorage<string>()
|
||||||
@@ -31,7 +31,7 @@ function normalizeHost(raw: string, port = '') {
|
|||||||
export function getBeastImageHost() {
|
export function getBeastImageHost() {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const explicit = String(config.imageComfyPrimaryHost || process.env.IMAGE_COMFY_PRIMARY_HOST || '').trim()
|
const explicit = String(config.imageComfyPrimaryHost || process.env.IMAGE_COMFY_PRIMARY_HOST || '').trim()
|
||||||
if (explicit) return normalizeHost(explicit)
|
if (explicit) return viaAgentFrontDoor(normalizeHost(explicit))
|
||||||
try {
|
try {
|
||||||
return getComfyHost()
|
return getComfyHost()
|
||||||
} catch {
|
} catch {
|
||||||
@@ -53,7 +53,7 @@ export function getSidecarImageHost() {
|
|||||||
]
|
]
|
||||||
for (const [raw, extraPort] of candidates) {
|
for (const [raw, extraPort] of candidates) {
|
||||||
if (!raw || isRetiredSidecarHost(raw)) continue
|
if (!raw || isRetiredSidecarHost(raw)) continue
|
||||||
const host = normalizeHost(raw, extraPort)
|
const host = viaAgentFrontDoor(normalizeHost(raw, extraPort))
|
||||||
if (host && !isRetiredSidecarHost(host)) return host
|
if (host && !isRetiredSidecarHost(host)) return host
|
||||||
}
|
}
|
||||||
return getBeastImageHost()
|
return getBeastImageHost()
|
||||||
@@ -61,8 +61,8 @@ export function getSidecarImageHost() {
|
|||||||
|
|
||||||
export function getImageComfyHost() {
|
export function getImageComfyHost() {
|
||||||
const fromAls = imageHostAls.getStore()
|
const fromAls = imageHostAls.getStore()
|
||||||
if (fromAls) return fromAls
|
if (fromAls) return viaAgentFrontDoor(fromAls) || fromAls
|
||||||
if (imageComfyHostOverride) return imageComfyHostOverride
|
if (imageComfyHostOverride) return viaAgentFrontDoor(imageComfyHostOverride) || imageComfyHostOverride
|
||||||
return getBeastImageHost() || getSidecarImageHost()
|
return getBeastImageHost() || getSidecarImageHost()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user