ComfyUI en headless : le faire tourner comme un service, l'utiliser de partout

Publié
11 juillet 2026
Mis à jour
11 juillet 2026
Par
Jacob Lloyd — rédigé avec l'aide de l'IA, une fois le projet terminé
Temps de lecture
10 min de lecture

En clair : Mon ordinateur de génération d'images fait tourner ComfyUI en permanence en arrière-plan, sans moniteur et sans personne connecté à un bureau. Je peux le démarrer, vérifier son état, ou lui demander une image en une seule ligne de commande, et je peux ouvrir son interface complète depuis mon ordinateur portable ou mon téléphone via une connexion privée chiffrée. Un petit plugin que j'ai construit garantit que chaque workflow enregistré depuis n'importe quel appareil atterrit sur cette même machine, pour que rien ne se disperse.

Dans le précédent article, j'avais fait tourner ComfyUI + Z-Image Turbo pour générer une image 1024px en ~27 secondes sur un mini PC AMD. Voici la suite : transformer cette installation en appareil toujours actif — pas de session bureau, pas de terminal à surveiller — que n'importe quel appareil de la maison peut utiliser, tandis que tout ce qu'elle produit et chaque workflow qu'elle enregistre restent sur cette seule machine.

En bref

  • Ce que c'est : ComfyUI tournant en headless sous un service utilisateur systemd, contrôlé par une petite CLI, piloté par script via son API HTTP, et atteint depuis d'autres appareils via un VPN maillé en HTTPS.
  • Ce que ça coûte : gratuit. Le palier de VPN qui permet ça (Tailscale personnel) est lui aussi gratuit.
  • Ce qu'il vous faut : une installation ComfyUI fonctionnelle et environ une soirée.
  • Ce que vous obtenez : comfyctl generate "..." depuis n'importe quel shell, l'interface complète sur votre ordinateur portable ou votre téléphone, et une bibliothèque de workflows qui vit dans un seul dossier sur le serveur, peu importe depuis quel appareil vous avez enregistré.

Ce que vous obtenez

Au quotidien, « utiliser la machine à images » se résume à l'une de ces trois choses, dont aucune n'implique de s'asseoir devant elle :

comfyctl status     # santé du serveur + état systemd + fichier de sortie le plus récent
comfyctl generate "a foggy harbor at dawn, cinematic"
comfyctl logs 100

…ou un script sur une autre machine qui envoie un prompt par POST à l'API HTTP, ou l'interface complète du graphe de nœuds ouverte dans un onglet de navigateur sur mon ordinateur portable à l'autre bout de la maison — en HTTPS, via le VPN, avec les enregistrements qui atterrissent sur le disque du serveur.

Pourquoi le headless

L'interface web sert à concevoir un workflow. C'est un moyen épouvantable d'en exécuter un pour la 200e fois. Une fois qu'un workflow est réglé, ce que vous voulez vraiment, c'est un appareil : une machine toujours allumée, toujours prête, qui répond à ce qui la sollicite — une ligne de commande shell, une tâche cron, un chatbot qui a besoin d'un nouvel avatar à 3 h du matin.

Le headless, ici, ne veut pas dire « aucune interface, jamais ». Le serveur n'a pas de session bureau ni de navigateur à lui, mais l'interface existe toujours, servie en HTTP à tout appareil qui la demande. Le serveur fait le travail, tout le reste n'est qu'un écran.

Le service utilisateur systemd

Le modèle de service est le même que dans l'article d'installation, donc je vais faire court. Une unité utilisateur systemd dans ~/.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     # garde les services utilisateur actifs sans personne connecté

Cette dernière ligne est le détail spécifique au headless que tout le monde rate : sans le linger, les unités utilisateur s'arrêtent à la fin de votre dernière session — ce qui, sur une machine headless, veut dire « immédiatement ». Le EnvironmentFile garde le port et les options de lancement dans un seul endroit éditable (comfy.env), pour que d'autres outils puissent lire la même valeur au lieu de coder 8188 en dur.

Un script de contrôle façon comfyctl

Le systemctl brut fonctionne, mais une fine enveloppe donne à la machine des allures de produit. La mienne fait environ 100 lignes de bash et s'appelle comfyctl :

comfyctl start      # systemctl start + interrogation de /system_stats jusqu'à ce que ce soit sain
comfyctl stop
comfyctl restart
comfyctl status     # santé + état de l'unité + dernier PNG de sortie
comfyctl generate "<prompt>" [--seed N --steps 8 --width 1024 --out CHEMIN]
comfyctl logs [N]   # journalctl --user -u comfyui.service -n N

Les détails qui rendent ça agréable plutôt que juste court :

  • Sain veut dire que l'API répond, pas que le processus existe. curl -sf http://127.0.0.1:$PORT/system_stats en boucle avec un budget de 60 secondes. Quand systemd dit « active », ça veut seulement dire que Python a démarré ; un serveur de modèle n'est « debout » que quand il répond en HTTP.
  • Il lit le port depuis le même fichier d'environnement que le service, pour qu'un changement de port à un seul endroit ne puisse pas laisser l'outil en rade.
  • generate démarre automatiquement le serveur. Si le contrôle de santé échoue, il démarre l'unité, attend qu'elle soit saine, puis soumet. Les appelants n'ont jamais besoin de savoir si la machine était réveillée.
  • status affiche le fichier le plus récent dans output/ — la réponse à « ma dernière génération a-t-elle vraiment fonctionné » sans rien avoir à ouvrir.

Piloter l’API HTTP depuis des scripts

Tout ce que fait l'interface passe par la même API HTTP, et la boucle centrale se résume à trois points d'entrée. Un workflow n'est que du JSON (exporté via Save (API format) dans l'interface) ; un script le charge, remplace le prompt et une nouvelle graine, puis :

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 du nœud de prompt

# 1. mise en file d'attente
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. interroger l'historique jusqu'à la fin
while True:
    hist = json.load(urllib.request.urlopen(f"{SERVER}/history/{pid}"))
    if pid in hist: break
    time.sleep(1)

# 3. récupérer l'image
img = hist[pid]["outputs"]["9"]["images"][0]           # id du nœud SaveImage
url = f"{SERVER}/view?filename={img['filename']}&subfolder={img['subfolder']}&type={img['type']}"
open("result.png", "wb").write(urllib.request.urlopen(url).read())

Uniquement la bibliothèque standard — pas de paquet client ComfyUI, pas de clé d'API. C'est le modèle qui se cache derrière comfyctl generate, et c'est la pièce qui transforme la machine en infrastructure : tout ce qui peut faire une requête HTTP peut désormais faire des images.

Le plugin Workbench : votre bibliothèque de workflows vit sur la machine

Voici le problème apparu dès la première semaine d'usage à distance. La fonction Export standard de ComfyUI enregistre le JSON du workflow via le mécanisme de téléchargement du navigateur — sur l'appareil depuis lequel vous naviguez. Utilisez l'interface depuis trois appareils, et votre bibliothèque de workflows se retrouve éparpillée dans trois dossiers Téléchargements, dont aucun n'est la machine qui exécute réellement les workflows.

J'ai donc construit un petit nœud personnalisé (« Workbench ») qui redresse la direction de la gravité. C'est un plugin custom_nodes ordinaire : le côté Python enregistre quelques routes sur le propre serveur web de ComfyUI, le côté JS ajoute un onglet dans la barre latérale. Il fait deux choses :

1. Enregistrement/ouverture dans un dossier de travail

Le bouton Save de la barre latérale envoie le graphe actuel par POST au plugin, qui l'écrit dans le dossier canonique user/default/workflows de ComfyUI sur le serveur — nom de fichier assaini, confiné à ce dossier, écriture atomique (fichier temporaire + renommage), et une copie .bak.json tournante de ce qu'il écrase. Open liste ce même dossier, du plus récent au plus ancien.

Le point essentiel : l'interface tourne dans un navigateur distant, mais l'enregistrement va sur le disque local du serveur. Enregistrez depuis l'ordinateur portable, ouvrez depuis le téléphone, exécutez depuis un script — une seule bibliothèque, un seul dossier, sur la machine qui a le GPU. (Sur ma machine, ce dossier est lié symboliquement à un simple répertoire ~/comfy/workflows, donc c'est aussi trivialement sauvegardé.) Comme le frontend ne fait que des appels relatifs à l'origine vers le serveur depuis lequel il a été chargé, il se comporte de façon identique en localhost et via un proxy — aucune configuration par appareil.

2. Scan des modèles manquants + téléchargements sur liste blanche

L'autre douleur de l'usage à distance : vous ouvrez un workflow et il réclame un fichier de modèle que la machine n'a pas. Télécharger 12 Go sur votre téléphone puis les réuploader est évidemment absurde. Le plugin scanne le workflow chargé face à chaque dossier de modèles enregistré, liste ce qui manque, et le télécharge côté serveur, directement dans le bon models/<dossier> — streamé vers un fichier .part, reprenable via HTTP Range, vérification sha256 optionnelle, avec la progression poussée vers l'interface par websocket.

Comme « le navigateur peut ordonner au serveur de télécharger des URL arbitraires sur le disque » est une phrase qui fait peur, c'est bien clôturé :

  • HTTPS uniquement, et le nom d'hôte doit correspondre à une liste blanche dans config.json (par ex. huggingface.co, civitai.com, des jokers comme *.hf.co) — et chaque saut de redirection est revalidé face à la même liste, puisque la redirection-vers-n'importe-où est le contournement classique des listes blanches.
  • Les noms de fichiers sont réduits à un nom de base assaini et doivent se terminer par une extension autorisée (.safetensors, etc.) ; la destination doit être un dossier de modèles enregistré — aucune traversée de chemin.
  • Les fichiers existants ne sont jamais écrasés, il y a un plafond de taille par téléchargement (30 Go par défaut), et les téléchargements sont mis en file un par un.

Accès distant sécurisé : VPN maillé + HTTPS

Sur cette machine, ComfyUI écoute uniquement sur 127.0.0.1 — codé en dur, pas une option. Il n'a pas de page de connexion, donc il ne doit jamais être exposé à internet, et honnêtement pas non plus au LAN brut. La réponse propre est un VPN maillé comme Tailscale : chaque appareil obtient une adresse privée chiffrée, rien n'est atteignable depuis l'extérieur de votre compte, et sa fonction serve agit comme un proxy inverse HTTPS local :

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

Désormais, https://<votre-machine>.<votre-tailnet>.ts.net:8443 sert l'interface complète, avec un vrai certificat TLS provisionné automatiquement, à vos seuls appareils. ComfyUI lui-même n'entend toujours que localhost — le proxy est la seule porte d'entrée.

HTTPS n'est pas ici une politesse optionnelle. Les navigateurs restreignent de plus en plus (presse-papiers, certains comportements worker/websocket) le HTTP brut depuis des origines non-localhost, et vous faites transiter vos prompts et vos images sur le réseau. Le VPN maillé vous donne un vrai certificat sans posséder de domaine ni ouvrir de port. Une remarque pratique : connectez-vous par le nom DNS de la machine, pas par son IP VPN brute — le certificat TLS est émis pour le nom, donc l'IP fera échouer la vérification du certificat.

Pièges

  • Les 403 qui n'arrivent qu'à distance sont des contrôles d'origine, pas d'authentification. Le middleware serveur de ComfyUI valide Origin/Host et consorts sur les requêtes qui changent l'état ; certaines configurations de proxy transmettent des en-têtes qui ne correspondent pas à ce qu'il attend (je suis tombé exactement là-dessus en branchant un autre outil — un 403 fondé sur Sec-Fetch-Site). Corrigez les en-têtes transmis par le proxy, ou restreignez les réglages CORS de ComfyUI (--enable-cors-header) à votre seule origine de confiance — jamais *.
  • Utilisez le nom d'hôte du VPN, pas l'IP. Le HTTPS via le VPN maillé fonctionne par certificat lié au nom ; taper l'adresse brute donne des erreurs TLS qui donnent l'impression que le serveur est cassé.
  • Les frontends de nœuds personnalisés doivent être relatifs à l'origine. Toute URL http://127.0.0.1:8188/... codée en dur dans le JS d'un plugin fonctionne sur la machine et casse silencieusement à travers le proxy. Utilisez api.fetchApi() de ComfyUI et des chemins relatifs.
  • Les websockets à travers un proxy peuvent se couper. Les événements de progression voyagent sur le websocket ; derrière un proxy, ça meurt parfois silencieusement. Workbench interroge en secours pour exactement cette raison — concevez en tenant compte de ça.
  • loginctl enable-linger, sinon votre « service » meurt à la déconnexion.
  • « L'unité est active » ≠ « le serveur est debout ».
  • Gardez la liste blanche de téléchargement stricte.
  • Ne redirigez jamais de port vers internet. Pas d'authentification + exécution arbitraire de workflow + (avec un téléchargeur) écritures sur le disque. VPN maillé ou rien.

Là où le premier article se terminait avec un ordinateur qui peut générer des images, celui-ci se termine par quelque chose de mieux : une machine tranquille sur une étagère que tous les appareils et scripts que je possède peuvent utiliser — et qui garde tout ce qu'elle produit à un seul endroit.


← Plus de IA et LLM locaux