ヘッドレスComfyUI:サービス化してどこからでも使う

公開日
2026年7月11日
更新日
2026年7月11日
著者
Jacob Lloyd — プロジェクト完了後、AIの支援を受けて執筆
読了時間
約13分で読めます

かんたんに言うと: 画像生成用のコンピューターが、モニターもデスクトップも無しでComfyUIを常時バックグラウンドで動かしています。コマンド一行で起動・確認・画像生成ができ、ノートPCやスマホからも暗号化されたプライベート接続で本来の画面を開けます。自作の小さなプラグインのおかげで、どの端末から保存してもワークフローは必ずその一台に集まり、散らばりません。

前回の記事では、AMDミニPC上でComfyUI + Z-Image Turboを動かし、1024pxの画像を約27秒で生成できるところまで作りました。今回はその続編です。このマシンを常時稼働のアプライアンスに変えます——デスクトップセッション無し、ターミナルの見張り無し。家中のどの端末からでも使えて、生成物も保存したワークフローも、すべてその一台に残るようにします。

要点

  • これは何: systemdユーザーサービスでヘッドレス稼働するComfyUI。小さなCLIで制御し、HTTP APIをスクリプトから叩き、メッシュVPN + HTTPSで他の端末から使う。
  • 費用: 無料。必要なVPN(Tailscaleの個人プラン)も無料。
  • 必要なもの: 動いているComfyUIと、ひと晩の作業時間。
  • 手に入るもの: どのシェルからでも comfyctl generate "..."、ノートPCやスマホでフルUI、そしてどの端末から保存してもサーバーの1フォルダに集まるワークフローライブラリ。

完成形

日常的に「画像生成マシンを使う」というのは、次の3つのどれかで、いずれも本体の前に座る必要はありません。

comfyctl status     # サーバーの健康状態 + systemdの状態 + 最新の出力ファイル
comfyctl generate "a foggy harbor at dawn, cinematic"
comfyctl logs 100

あるいは、別のマシン上のスクリプトがHTTP APIにプロンプトをPOSTする。あるいは、家の反対側のノートPCのブラウザでノードグラフのフルUIを開く——HTTPS、VPN経由、保存先はサーバーのディスク。

なぜヘッドレスか

Web UIはワークフローを設計するための道具です。同じワークフローを200回目に実行する道具としては最悪です。設定が固まったら、本当に欲しいのはアプライアンス——常時起動していて、いつでも応答し、シェルの一行コマンドでも、cronジョブでも、深夜3時に新しいアバターが欲しいチャットボットでも、頼まれれば応えるマシンです。

ヘッドレスといっても「UIが一切ない」わけではありません。サーバー自身にはデスクトップセッションもブラウザもありませんが、UIはHTTPで配信されるので、見たい端末で開けばいい。仕事はサーバーが、画面はそれ以外が担当する、というだけです。

systemdユーザーサービス

サービス化のパターンはセットアップ編と同じなので手短に。~/.config/systemd/user/comfyui.service にsystemdのユーザーユニットを置きます:

[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     # 誰もログインしていなくてもユーザーサービスを維持

最後の行がヘッドレス特有の見落としポイントです。lingerを有効にしないと、ユーザーユニットは最後のセッション終了と同時に止まります——ヘッドレスマシンでは「即座に」という意味です。EnvironmentFile でポートや起動フラグを1か所(comfy.env)にまとめておけば、他のツールも8188をハードコードせずに同じ値を読めます。

comfyctl風の制御スクリプト

素の systemctl でも動きますが、薄いラッパーを被せるとマシンが「製品」らしくなります。私のは約100行のbashで、名前は comfyctl

comfyctl start      # systemctl start + /system_stats が応答するまでポーリング
comfyctl stop
comfyctl restart
comfyctl status     # 健康状態 + ユニット状態 + 最新の出力PNG
comfyctl generate "<プロンプト>" [--seed N --steps 8 --width 1024 --out PATH]
comfyctl logs [N]   # journalctl --user -u comfyui.service -n N

「短い」だけでなく「快適」にしている工夫:

  • 健康 = APIが応答すること。プロセスの存在ではない。 curl -sf http://127.0.0.1:$PORT/system_stats を60秒の猶予付きでループ。systemdの「active」はPythonが起動しただけの意味で、モデルサーバーはHTTPに応答して初めて「起きた」と言えます。
  • ポートはサービスと同じenvファイルから読むので、1か所の変更でツールが置き去りになりません。
  • generate はサーバーを自動起動。 ヘルスチェックに失敗したらユニットを起動し、健康になるのを待ってから投入。呼び出し側はマシンが起きているかどうかを知らなくていい。
  • statusoutput/ の最新ファイルを表示——「さっきの生成は成功したのか」に何も開かずに答えます。

HTTP APIをスクリプトから叩く

UIの操作はすべて同じHTTP APIを通っていて、コアのループはエンドポイント3つです。ワークフローはただのJSON(UIの「保存(API形式)」でエクスポート)。スクリプトで読み込み、プロンプトと新しいシードを差し替えて:

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

# 1. キューに投入
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. 完了まで履歴をポーリング
while True:
    hist = json.load(urllib.request.urlopen(f"{SERVER}/history/{pid}"))
    if pid in hist: break
    time.sleep(1)

# 3. 画像を取得
img = hist[pid]["outputs"]["9"]["images"][0]           # SaveImageノードのid
url = f"{SERVER}/view?filename={img['filename']}&subfolder={img['subfolder']}&type={img['type']}"
open("result.png", "wb").write(urllib.request.urlopen(url).read())

標準ライブラリのみ——ComfyUIクライアントパッケージもAPIキーも不要。これが comfyctl generate の中身のパターンで、このマシンをインフラに変えた部分です。HTTPリクエストを出せるものは何でも、画像を作れるようになります。

Workbenchプラグイン:ワークフローライブラリはマシンの上に

リモート運用を始めて最初の週に出てきた問題がこれです。ComfyUI標準の「エクスポート」は、ワークフローJSONをブラウザのダウンロード機能経由で——つまり閲覧している端末に保存します。3台の端末からUIを使えば、ワークフローライブラリは3つのダウンロードフォルダに散らばり、しかもそのどれも実際にワークフローを実行するマシンではない。

そこで、重力の向きを直す小さなカスタムノード(「Workbench」)を作りました。普通の custom_nodes プラグインで、Python側がComfyUI自身のWebサーバーにルートをいくつか登録し、JS側がサイドバータブを追加します。仕事は2つ:

1. 作業フォルダへの保存と読み込み

サイドバーの保存ボタンは現在のグラフをプラグインにPOSTし、プラグインがそれをサーバー上のComfyUI正規フォルダ user/default/workflows に書き込みます——ファイル名をサニタイズし、そのフォルダ内に限定し、アトミックに書き込み(一時ファイル + rename)、上書き時は直前のファイルを .bak.json として1世代残します。「開く」は同じフォルダを新しい順に一覧表示。

要点はここです:UIはリモートのブラウザで動いているのに、保存はサーバーのローカルディスクに行く。ノートPCから保存し、スマホから開き、スクリプトから実行——ライブラリは一つ、フォルダは一つ、GPUのあるマシンの上。(私の環境ではそのフォルダを普通の ~/comfy/workflows にシンボリックリンクしているので、バックアップも簡単です。)フロントエンドは自分を配信したサーバーへのオリジン相対の呼び出ししかしないので、localhostでもプロキシ越しでも端末ごとの設定なしで同じように動きます。

2. 不足モデルのスキャン + 許可リスト式ダウンロード

リモート運用のもう一つの痛点:ワークフローを開いたら、マシンに無いモデルファイルを要求される。12GBをスマホにダウンロードして再アップロードするのは明らかに間違っています。プラグインは読み込んだワークフローを登録済みの全モデルフォルダと照合し、不足分を一覧表示して、サーバー側で正しい models/<フォルダ> に直接ダウンロードします——.part ファイルへのストリーミング、HTTP Rangeによる再開、任意のsha256検証、websocket経由のUIへの進捗通知付き。

「ブラウザがサーバーに任意URLのダウンロードを命じられる」というのは怖い文なので、柵で囲ってあります:

  • HTTPSのみ。ホスト名は config.json の許可リスト(例:huggingface.cocivitai.com*.hf.co のようなワイルドカード)に一致必須——さらにリダイレクトの各ホップも同じリストで再検証します。どこへでもリダイレクトできるのは許可リスト回避の古典なので。
  • ファイル名はサニタイズしたベース名に縮め、許可リストの拡張子(.safetensors など)で終わること。保存先は登録済みモデルフォルダのみ——パストラバーサル不可。
  • 既存ファイルは決して上書きしない。ダウンロード1件あたりのサイズ上限(デフォルト30GB)あり。同時ダウンロードは1件ずつでキュー処理。

安全なリモートアクセス:メッシュVPN + HTTPS

このマシンのComfyUIは 127.0.0.1 だけで待ち受けています——フラグではなくハードコードです。ログイン画面が無いので、インターネットに公開するのは絶対に駄目ですし、正直、生のLANに開くのもお勧めしません。きれいな答えはTailscaleのようなメッシュVPNです。全端末がプライベートな暗号化アドレスを持ち、自分のアカウントの外からは何も届かず、serve 機能がローカルHTTPSリバースプロキシとして働きます:

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

これで https://<マシン名>.<あなたのtailnet>.ts.net:8443 が、自動発行される正規のTLS証明書付きで、あなたの端末だけにフルUIを配信します。ComfyUI自身は相変わらずlocalhostからの声しか聞きません——プロキシが唯一の出入口です。

HTTPSは「あれば丁寧」ではなく必須です。ブラウザはlocalhost以外の平文HTTPオリジンで機能をどんどん制限しますし(クリップボード、一部のworker/websocket挙動)、プロンプトと画像がネットワークを流れるのですから。メッシュVPNなら、ドメインを持たずポートも開けずに本物の証明書が手に入ります。実用上の注意を一つ:接続は生のVPN IPではなくマシンのDNS名で。TLS証明書は名前に対して発行されるので、IPだと証明書チェックに失敗します。

ハマりどころ

  • リモートでだけ出る403は認証ではなくオリジンチェック。 ComfyUIのサーバーミドルウェアは状態変更リクエストの Origin/Host 等を検証します。プロキシ構成によっては期待と合わないヘッダーが転送されます(別ツールを繋いだ時にまさにこれ——Sec-Fetch-Site 起因の403——を踏みました)。プロキシの転送ヘッダーを直すか、ComfyUIのCORS設定(--enable-cors-header)を信頼する1オリジンに絞ってください——* は絶対に駄目。
  • VPNのIPではなくホスト名で接続。 メッシュVPNのHTTPSは名前ごとの証明書なので、生アドレスを叩くとサーバーが壊れたように見えるTLSエラーになります。
  • カスタムノードのフロントエンドはオリジン相対で。 プラグインのJSに http://127.0.0.1:8188/... がハードコードされていると、本体上では動くのにプロキシ越しでは静かに壊れます。ComfyUIの api.fetchApi() と相対パスを使うこと。
  • プロキシ越しのwebsocketは切れることがある。 進捗イベントはwebsocketに乗りますが、プロキシの後ろでは時々静かに死にます。Workbenchがフォールバックとしてポーリングも持つのはまさにこのため——切れる前提で設計を。
  • loginctl enable-linger を忘れるとログアウトで「サービス」が死ぬ。 ヘッドレスマシンでsystemdのユーザーユニットが「なぜか止まる」原因のダントツ1位です。
  • 「ユニットがactive」≠「サーバーが起きている」。 モデルの読み込みには時間がかかります。スクリプトでは必ず /system_stats のようなHTTPエンドポイントを、きちんとしたタイムアウト付きでヘルスチェックしてから成功を宣言すること。
  • ダウンロード許可リストは狭く保つ。 リダイレクトを再検証しないホスト名許可リストは安心感の偽物です。ダウンローダー系プラグインを作る・導入するなら、リダイレクトを手動で追い、各ホップを再チェックしているか確認を。
  • 絶対にインターネットへポート転送しないこと。 認証なし + 任意ワークフロー実行 + (ダウンローダーがあれば)ディスクへの書き込み。メッシュVPNか、さもなくば何もしない。

最初の記事は「画像を生成できるコンピューター」で終わりました。今回はもっと良いところで終わります:棚の上で静かに動き、手持ちのどの端末やスクリプトからでも使えて、作ったものを全部一か所に保管してくれるマシンです。


← AIとローカルLLMをもっと見る