ComfyUI sin interfaz: ejecútalo como servicio, úsalo desde cualquier lugar

Categoría
IA y LLM Local
Publicado
11 julio 2026
Actualizado
11 julio 2026
Por
Jacob Lloyd — escrito con ayuda de IA, después del proyecto
Tiempo de lectura
10 min de lectura

En palabras simples: Mi ordenador de generación de imágenes ejecuta ComfyUI todo el tiempo en segundo plano, sin monitor y sin nadie conectado a un escritorio. Puedo arrancarlo, revisarlo o pedirle una imagen con un comando de una sola línea, y puedo abrir su interfaz completa desde mi portátil o mi móvil a través de una conexión privada y cifrada. Un pequeño plugin que construí se asegura de que cada workflow que guardo desde cualquier dispositivo termine en ese único ordenador, para que nada quede disperso.

En el artículo anterior conseguí que ComfyUI + Z-Image Turbo generaran una imagen de 1024px en ~27 segundos en un mini PC con AMD. Esta es la continuación: convertir esa instalación en un electrodoméstico siempre encendido — sin sesión de escritorio, sin niñera de terminal — que cualquier dispositivo de la casa puede usar, mientras todo lo que produce y cada workflow que guarda se queda en esa única máquina.

Resumen

  • Qué es: ComfyUI ejecutándose sin interfaz bajo un servicio de usuario de systemd, controlado por un pequeño CLI, manejado por script a través de su API HTTP, y accesible desde otros dispositivos mediante una VPN de malla con HTTPS.
  • Qué cuesta: gratis. El nivel de VPN que hace esto (Tailscale personal) también es gratis.
  • Qué necesitas: una instalación de ComfyUI funcionando y más o menos una tarde.
  • Con qué te quedas: comfyctl generate "..." desde cualquier shell, la interfaz completa en tu portátil o móvil, y una biblioteca de workflows que vive en una sola carpeta del servidor sin importar desde qué dispositivo guardaste.

Con qué te quedas

Día a día, "usar la máquina de imágenes" se reduce a una de tres cosas, ninguna de las cuales implica sentarse frente a ella:

comfyctl status     # salud del servidor + estado de systemd + archivo de salida más reciente
comfyctl generate "a foggy harbor at dawn, cinematic"
comfyctl logs 100

...o un script en otra máquina haciendo POST de un prompt a la API HTTP, o la interfaz completa de grafo de nodos abierta en una pestaña del navegador de mi portátil al otro lado de la casa — por HTTPS, a través de la VPN, con los guardados aterrizando en el disco del servidor.

Por qué sin interfaz

La interfaz web es la forma de diseñar un workflow. Es una forma pésima de ejecutarlo por vigésima vez. Una vez que un workflow está afinado, lo que realmente quieres es un electrodoméstico: una máquina que siempre está encendida, siempre lista, y responde a lo que sea que se lo pida — una línea de comandos, una tarea cron, un chatbot que necesita un avatar nuevo a las 3 de la madrugada.

Sin interfaz aquí no significa "nunca hay interfaz". El servidor no tiene sesión de escritorio ni navegador propio; la interfaz sigue existiendo, servida por HTTP a cualquier dispositivo que la quiera. El servidor hace el trabajo, todo lo demás es solo una pantalla.

El servicio de usuario systemd

El patrón de servicio es el mismo que en el artículo de configuración, así que lo dejo breve. Una unidad de usuario de systemd en ~/.config/systemd/user/comfyui.service:

[Unit]
Description=ComfyUI server
After=network-online.target

[Service]
ExecStart=%h/comfy/start-comfyui.sh
WorkingDirectory=%h/comfy/ComfyUI
EnvironmentFile=-%h/comfy/comfy.env
Restart=on-failure
RestartSec=5
TimeoutStartSec=120

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user enable --now comfyui.service
loginctl enable-linger $USER     # mantiene los servicios de usuario en marcha con nadie conectado

Esa última línea es la parte específica de "sin interfaz" que la gente pasa por alto: sin el linger, las unidades de usuario se detienen cuando termina tu última sesión — lo que en una máquina sin interfaz es "inmediatamente". El EnvironmentFile mantiene el puerto y las opciones de arranque en un solo lugar editable (comfy.env), para que otras herramientas puedan leer el mismo valor en lugar de dejar el 8188 fijo en el código.

Un script de control al estilo comfyctl

El systemctl a pelo funciona, pero un envoltorio ligero hace que la máquina se sienta como un producto. El mío tiene ~100 líneas de bash y se llama comfyctl:

comfyctl start      # systemctl start + sondea /system_stats hasta que esté saludable
comfyctl stop
comfyctl restart
comfyctl status     # salud + estado de la unidad + PNG de salida más reciente
comfyctl generate "<prompt>" [--seed N --steps 8 --width 1024 --out PATH]
comfyctl logs [N]   # journalctl --user -u comfyui.service -n N

Los detalles que lo hacen agradable y no solo breve:

  • Saludable significa que la API responde, no que el proceso existe. curl -sf http://127.0.0.1:$PORT/system_stats en un bucle con un presupuesto de 60 segundos. Que systemd diga "active" solo significa que Python arrancó; un servidor de modelo no está listo hasta que responde por HTTP.
  • Lee el puerto del mismo archivo de entorno que usa el servicio, así que cambiar el puerto en un solo lugar no puede dejar a la herramienta desincronizada.
  • generate arranca el servidor automáticamente. Si falla la comprobación de salud, arranca la unidad, espera a que esté saludable y luego envía. Quien lo llame nunca necesita saber si la máquina estaba despierta.
  • status imprime el archivo más reciente en output/ — la respuesta a "¿de verdad funcionó mi última generación?" sin abrir nada.

Manejar la API HTTP desde scripts

Todo lo que hace la interfaz pasa por la misma API HTTP, y el bucle central son tres endpoints. Un workflow es simplemente JSON (se exporta con Save (API format) en la interfaz); un script lo carga, sustituye el prompt y una semilla nueva, y:

import json, time, urllib.request

SERVER = "http://127.0.0.1:8188"
graph = json.load(open("workflow_api.json"))
graph["6"]["inputs"]["text"] = "a foggy harbor at dawn, cinematic"   # id de tu nodo de prompt

# 1. encolarlo
req = urllib.request.Request(f"{SERVER}/prompt",
    data=json.dumps({"prompt": graph}).encode(),
    headers={"Content-Type": "application/json"})
pid = json.load(urllib.request.urlopen(req))["prompt_id"]

# 2. sondear el historial hasta que termine
while True:
    hist = json.load(urllib.request.urlopen(f"{SERVER}/history/{pid}"))
    if pid in hist: break
    time.sleep(1)

# 3. obtener la imagen
img = hist[pid]["outputs"]["9"]["images"][0]           # id de tu nodo SaveImage
url = f"{SERVER}/view?filename={img['filename']}&subfolder={img['subfolder']}&type={img['type']}"
open("result.png", "wb").write(urllib.request.urlopen(url).read())

Solo biblioteca estándar — sin paquete cliente de ComfyUI, sin clave de API. Este es el patrón detrás de comfyctl generate, y es la pieza que convierte la máquina en infraestructura: cualquier cosa capaz de hacer una petición HTTP puede ahora generar imágenes.

El plugin Workbench: tu biblioteca de workflows vive en la máquina

Aquí está el problema que apareció la primera semana de uso remoto. El Export de serie de ComfyUI guarda el JSON del workflow a través del mecanismo de descarga del navegador — en el dispositivo desde el que estés navegando. Usa la interfaz desde tres dispositivos y tu biblioteca de workflows queda dispersa en tres carpetas de Descargas, ninguna de ellas la máquina que realmente ejecuta los workflows.

Así que construí un pequeño nodo personalizado ("Workbench") que corrige la dirección de la gravedad. Es un plugin normal de custom_nodes: el lado Python registra unas cuantas rutas en el propio servidor web de ComfyUI, el lado JS añade una pestaña en la barra lateral. Hace dos trabajos:

1. Guardar/abrir en la carpeta de trabajo

El botón Guardar de la barra lateral hace POST del grafo actual al plugin, que lo escribe en la carpeta canónica de ComfyUI user/default/workflows en el servidor — con nombre de archivo saneado, confinado a esa carpeta, escritura atómica (archivo temporal + rename), y una copia rotativa .bak.json de lo que sobrescriba. Abrir lista esa misma carpeta, del más nuevo al más viejo.

El punto clave: la interfaz corre en un navegador remoto, pero el guardado va al disco local del servidor. Guarda desde el portátil, abre desde el móvil, ejecuta desde un script — una sola biblioteca, una sola carpeta, en la máquina con la GPU. (En mi máquina esa carpeta está enlazada simbólicamente a un directorio normal ~/comfy/workflows, así que también es trivial hacerle copia de seguridad.) Como el frontend solo hace llamadas relativas al origen que lo sirvió, se comporta igual en localhost que a través de un proxy — sin configuración por dispositivo.

2. Escaneo de modelos faltantes + descargas con lista blanca

El otro dolor del uso remoto: abres un workflow y pide un archivo de modelo que la máquina no tiene. Descargar 12 GB al móvil y volver a subirlos es obviamente un error. El plugin escanea el workflow cargado contra cada carpeta de modelos registrada, lista lo que falta, y lo descarga en el servidor, directamente en la carpeta correcta models/<carpeta> — transmitido a un archivo .part, reanudable por HTTP Range, verificación sha256 opcional, con el progreso enviado a la interfaz por websocket.

Como "el navegador puede decirle al servidor que descargue URLs arbitrarias a disco" es una frase que da miedo, está vallado:

  • Solo HTTPS, y el nombre de host debe coincidir con una lista blanca en config.json (por ejemplo huggingface.co, civitai.com, comodines como *.hf.co) — y cada salto de redirección se vuelve a validar contra la misma lista, ya que redirigir a cualquier parte es el bypass clásico de una lista blanca.
  • Los nombres de archivo se reducen a un nombre base saneado y deben terminar en una extensión de la lista blanca (.safetensors, etc.); el destino debe ser una carpeta de modelos registrada — sin traversal de rutas.
  • Los archivos existentes nunca se sobrescriben, hay un límite de tamaño por descarga (30 GB por defecto), y las descargas se encolan de una en una.

Acceso remoto seguro: VPN de malla + HTTPS

ComfyUI en esta máquina escucha solo en 127.0.0.1 — fijado en el código, no un flag. No tiene página de inicio de sesión, así que nunca debe exponerse a internet, y sinceramente tampoco a la LAN cruda. La respuesta limpia es una VPN de malla como Tailscale: cada dispositivo recibe una dirección privada cifrada, nada es accesible desde fuera de tu cuenta, y su función serve actúa como un proxy inverso HTTPS local:

tailscale serve --bg --https=8443 http://127.0.0.1:8188

Ahora https://<tu-maquina>.<tu-tailnet>.ts.net:8443 sirve la interfaz completa, con un certificado TLS real (aprovisionado automáticamente), solo a tus dispositivos. ComfyUI en sí sigue escuchando únicamente a localhost — el proxy es la única puerta.

HTTPS no es una cortesía opcional aquí. Los navegadores restringen cada vez más (portapapeles, cierto comportamiento de worker/websocket) el HTTP plano desde orígenes que no son localhost, y estás enviando tus prompts e imágenes por la red. La VPN de malla te da un certificado real sin tener que ser dueño de un dominio ni abrir un puerto. Una nota práctica: conéctate por el nombre DNS de la máquina, no por su IP cruda de VPN — el certificado TLS se emite para el nombre, así que la IP fallará la comprobación del certificado.

Trampas

  • Los 403 que solo ocurren en remoto son comprobaciones de origen, no de autenticación. El middleware del servidor de ComfyUI valida Origin/Host y similares en peticiones que cambian estado; algunas configuraciones de proxy reenvían cabeceras que no coinciden con lo esperado (me pasó exactamente esto al conectar otra herramienta — un 403 basado en Sec-Fetch-Site). Corrige las cabeceras reenviadas del proxy, o acota la configuración CORS de ComfyUI (--enable-cors-header) a tu único origen de confianza — nunca *.
  • Usa el nombre de host de la VPN, no la IP. El HTTPS a través de la VPN de malla es por certificado-por-nombre; acceder a la dirección cruda da errores de TLS que parecen un servidor roto.
  • Los frontends de nodos personalizados deben ser relativos al origen. Cualquier http://127.0.0.1:8188/... fijo en el JS de un plugin funciona en la máquina y se rompe silenciosamente a través del proxy. Usa api.fetchApi() de ComfyUI y rutas relativas.
  • Los websockets a través de un proxy pueden caerse. Los eventos de progreso viajan por el websocket; detrás de un proxy a veces mueren en silencio. Workbench sondea como alternativa exactamente por esta razón — diséñalo esperando que pase.
  • loginctl enable-linger o tu "servicio" muere al cerrar sesión.
  • "La unidad está activa" ≠ "el servidor está listo".
  • Mantén la lista blanca de descargas estrecha.
  • Nunca redirijas esto a internet mediante port-forward. Sin autenticación + ejecución arbitraria de workflows + (con un descargador) escritura en disco. VPN de malla o nada.

Donde el primer artículo terminaba con un ordenador que puede generar imágenes, este termina con algo mejor: una máquina silenciosa en un estante que cualquier dispositivo o script que tengo puede usar — y que guarda todo lo que hace en un solo lugar.


← Más de IA y LLM Local