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:
Towsty
2026-09-02 08:04:11 -05:00
co-authored by Cursor
parent 2ae5f6fb2e
commit bdec8872b6
5 changed files with 99 additions and 36 deletions
+5 -2
View File
@@ -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=
+1
View File
@@ -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 || '',
+36 -18
View File
@@ -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
View File
@@ -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() }
} }
} }
+5 -5
View File
@@ -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()
} }