⚠️ 注意: AI画像生成時は著作権・肖像権にご注意ください。商用利用前には各サービスの利用規約をご確認ください。当ブログは生成された画像に関する責任を負いかねます。
📝 本記事にはアフィリエイトリンクが含まれています。
要約
スマホのDiscordアプリから /rumina と打つだけで、自分専用のAI画像生成が走る環境を作った話です。ComfyUI + 自前のcheckpointをRunPod Serverlessで動かし、Cloudflare Workersで入口・出口を作ってDiscord Botとつなぐ構成です。サーバー常駐なし・ストレージサービスなし・常時課金なしで実現できました。ハマりポイントを正直に書いているので、同じ構成を試したい方の参考になれば。
はじめに
BOOTH で画像集を販売するようになってから、作業フローに小さなストレスが積み重なっていました。
表情差分・衣装差分の確認って、PC前に座ってComfyUIを起動して、プロンプトを調整して、生成を待って…という流れが基本ですよね。ちょっと確かめたいだけなのに腰を上げるのが面倒で、「外出先でもスマホから気軽に生成を走らせたいな」と思うようになりました。
もちろんAPI経由で外部サービスに投げる手もあるんですが、僕は自分で用意したcheckpointを使いたいんです。商業向けサービスのベースモデルではなく、自分のスタイルに合わせたモデルで動かしたい。
そこで作ったのが今回の構成です。ComfyUI + 自前checkpoint を RunPod Serverless で動かし、Cloudflare Workers + Discord Bot を経由してスマホから叩けるようにしました。完全に自分専用のプライベート環境で、第三者への画像生成サービス提供ではありません。
結論から言うと、ちゃんと動きました。スマホから /rumina を打つと画像が返ってきます。ただ、思っていたより各所でハマりました。特に工程③(Cloudflare Workers + Discord Bot)は約3時間。その全記録をそのまま残しておきます。
全体構成:ComfyUI + RunPod Serverless + Discord の連携フロー
まず全体のアーキテクチャを押さえておくと、後の工程がぐっと分かりやすくなります。
[スマホ Discordアプリ]
│ ① /rumina コマンドでパラメータを渡す
▼
[Cloudflare Worker] ← 入口・出口の2ルートを1つのWorkerに持たせる
│ ② RunPodに /run で投げる(webhook=自分の出口URLを指定)→ 即終了(待たない)
│ ③ Discordへ type:5 を返す(3秒ルール。スマホには「考え中...」が出る)
▼
[RunPod Serverless]
│ ・起動時に GHCR(private)からイメージをpull(ComfyUI + checkpoint)
│ ・ComfyUIで生成
│ ④ 完了したらWebhookでWorkerの出口を叩く(画像base64を同梱)
▼
[Cloudflare Worker(出口ルート)]
│ ⑤ 受け取った画像をそのままDiscordに添付投稿
▼
[スマホ Discordアプリ] ← 画像が表示される

ポイントはCloudflare Workerが入口と出口を兼ねているところです。Discordからのコマンドを受けて RunPod にジョブを投げ(入口)、RunPod の生成完了 Webhook を受け取ってDiscordに返す(出口)。この2ルートを1つのWorkerで処理しています。
設計の判断ポイント
「なぜその技術を選んだか」を先に整理しておきます。構成を理解する上で、ここが一番大事なところだと思っています。
ネットワークボリュームではなくイメージ焼き込みにした理由
RunPod Serverlessでモデルを持たせる方法は大きく2つあります。ネットワークボリューム(共有ストレージにモデルを置く)か、Dockerイメージへの焼き込み(イメージ内にモデルを含める)かですね。
ネットワークボリュームはイメージが軽くなる反面、Serverless環境での接続設定が煩雑で、ボリューム自体に月額費用がかかります。今回は「コールドスタート時にイメージごと持ってくる」方式にしました。イメージサイズは大きくなります(Dockerfileの RUN curl でcheckpointを焼き込むため)が、ボリューム管理が不要で構成がシンプルになります。
Docker Hub ではなく GHCR にした理由
Docker Hub の private リポジトリは、無料プランだと1つまでです。複数モデルを管理したくなったときに詰まります。その点、GitHub Container Registry(GHCR)は GitHub のプライベートリポジトリに付随して使えるので、実質無料で管理できます。GitHub Actions でビルドしてそのままpushする流れも自然に組めるのがいいところです。
LINE ではなく Discord にした理由
「スマホから叩く」なら LINE Bot を検討する方もいると思います。ただ LINE の場合、生成した画像を返すのに一度どこかのストレージ(S3やR2など)へアップロードしてURLを渡す必要があるんです。
Discord Bot は 画像ファイルをそのままメッセージに添付できます。RunPod から返ってくる base64 の画像データを Blob に変換して添付するだけで済むので、ストレージが一切いりません。
常駐サーバーではなく Cloudflare Workers にした理由
Discord Bot のバックエンドとして VPS を常駐させる構成は一般的ですが、それだと月額費用が固定でかかってしまいます。Cloudflare Workers はリクエスト数課金で、無料枠(1日10万リクエスト)の範囲で十分に動きます。自分専用のBotならそんなに叩かないので、実質タダです。
しかも RunPod から Webhook を受け取る口もWorkerに作れるので、「Discord Bot のバックエンド」と「画像受け取り口」を1つのWorkerにまとめられます。
事前準備:必要なアカウント・ツール
各工程に入る前に、以下を用意しておくとスムーズです。
- GitHubアカウント(GHCRとGitHub Actions用)
- RunPodアカウント(Serverlessエンドポイント作成用)
- Cloudflare アカウント(Workers デプロイ用)
- Discord アカウント(Developer Portal でBot作成用)
- Node.js(Workers のローカル開発・デプロイ用)
- Docker(イメージのローカルテスト用。ビルド自体はActions で動かすので任意)
RunPodのアカウント登録やチャージがまだの方は、こちらに手順をまとめています。

この記事はServerless(APIから叩く構成)の話ですが、まずGUIでComfyUIを触ってワークフローを固めたいなら、Pod上でComfyUIを動かす手順から入るほうが早いと思います。

工程①:GHCRイメージのビルド & push
ComfyUI + worker-comfyui + 自前checkpointを含むDockerイメージを GHCR に push する工程です。
ビルドはGitHub Actions に任せて、そのまま push する構成にしました。ローカルでビルドすると時間がかかりすぎる(checkpointのダウンロードだけで相当待たされます)のと、Actions のキャッシュを使いたかったからです。
つまづき①:Civitai のダウンロードURL
Dockerfileの RUN curl でモデルをダウンロードしようとして、いきなりハマりました。
Civitai のサイトに表示されるモデルURLの番号は「モデルID」なんですが、ダウンロードに必要なのは「バージョンID」で、これが別物なんです。GET /api/v1/models/{モデルID} APIを叩いてバージョン一覧を確認し、目的のバージョンのIDを取ってくる必要があります。
さらに、Civitai は2026年4月からドメインが分割されていて、モデルによっては civitai.com ではなく civitai.red を使わないといけません。ここで「なぜかダウンロードできない」とけっこう悩みました。ドメインはダウンロードURLと照らし合わせて確認してください。
GET /api/v1/models/{モデルID} でバージョンIDを取得しましょう。加えて2026年4月以降はドメインが分割されているため、モデルによっては civitai.red を使う必要があります。つまづき②:GHCRイメージ名の大文字問題
GHCR のイメージ名(ghcr.io/{owner}/{name})は、すべて小文字でないと通りません。GitHub のユーザー名やリポジトリ名に大文字が入っていると「repository name must be lowercase」エラーになります。
Actions の env: コンテキストでは ${{ lower(...) }} テンプレート式が使えないので、bash の ,, 演算子で変換しました。
echo "IMAGE=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/coolat-serverless-gen" >> $GITHUB_ENV

ビルド時間
実際のビルド時間は 25分23秒でした(free-disk-space + Civitai DL + Dockerビルド + GHCR push を含む)。初回はそれなりに覚悟しておいたほうがいいです。

工程②:RunPod エンドポイント作成
GHCR のイメージを使って、RunPod Serverless のエンドポイントを作る工程です。RunPod のアカウントがまだの方はここから登録できます。
紹介リンクからGoogleアカウントで登録して$10以上チャージすると、$5〜$500のクレジットボーナスがもらえます
ボーナスの対象はGoogle登録のみ。手順は通常登録と同じです

つまづき①:GHCR 認証は classic PAT のみ
RunPod の Serverless 設定には、プライベートレジストリの認証情報を入れる欄があります。ここで GitHub の PAT(Personal Access Token)を使うんですが、fine-grained PAT は GHCR の認証に対応していません。classic PAT の read:packages スコープで発行してください。
つまづき②:GPU 枯渇
最初は 24GB クラスだけを選んでいたんですが、深夜のピーク時間帯に「枯渇」してジョブが動きませんでした。検証中は 48GB(High Supply)を足したら安定したので、動作確認が終わるまではそのまま48GBで回していました。

ここで大事なのは、GPUは1つを選ぶのではなく複数チェックできるという点です。チェックした中から空いているものが使われて、右側の 1st 2nd で優先順位も指定できます。つまり「普段は安いGPU、埋まっていたら高いGPUにフォールバック」という組み方ができるわけです。
今は動作確認が終わったので、普段は 24GB($0.69/hr)だけにして節約しています。どうしても空きがなくて動かないときだけ 48GB($1.22/hr)にもチェックを入れて、さくっと生成してから戻す、という運用ですね。生成が数分で終わる用途なら、この付け外しだけで十分間に合います。
ちなみに現行のコンソールでは、GPUを「RTX 4090」のような製品名ではなく VRAM容量のクラス(16GB / 24GB / 48GB…)で選ぶ形式になっています。24GBクラスの中身は RTX 3090・L4・RTX A5000・PRO 6000 MIG 24GB あたりで、どれが割り当てられるかは選べません。
つまづき③:/runsync のタイムアウト
動作確認で curl を使って /runsync を叩いていたんですが、コールドスタートに約4分30秒かかるので、デフォルトのタイムアウトで切れてしまいます。テストするときは --max-time 300(5分)以上を指定してください。
本番ではこの問題を避けるため、Webhook方式(非同期の /run)を使っています。
つまづき④:画像返却キーの Breaking Change
worker-comfyui v5.0.0 以降、画像データのレスポンスキーが変わっています。
- 旧:
output.images[0].image - 新:
output.images[0].data
README にはちゃんと書いてあったんですが、サンプルコードをそのまま流用すると「画像が返ってこない」状態になります。使っているバージョンを確認して、キーを合わせてください。
output.images[0].image でアクセスしても undefined になります。使用しているバージョンを確認して、v5以降なら output.images[0].data に書き換えてください。ここに挙げたのはServerless固有のつまづきですが、RunPod全般のエラーや起動しない系のトラブルは別記事にまとめています。

工程③:Cloudflare Workers & Discord Bot
この工程が一番ボリュームが多くて、約3時間かかりました。ハマった順に書いていきます。
構成の概要
Cloudflare Workers のコードは1つだけ。URLパスで入口と出口を振り分けています。
POST /→ Discord Interaction Endpoint(コマンド受付 + RunPod への投げ)POST /webhook→ RunPod からの完了通知受け取り + Discord への画像送信
つまづき①:discord-interactions が Workers 環境で動かない
Discord の署名検証に discord-interactions パッケージを使おうとしたら、Cloudflare Workers 環境で型エラーが出て動きませんでした。このパッケージが Node.js の Buffer や express に依存していて、Workers の型定義(@cloudflare/workers-types)と競合してしまうんですね。
解決策は、Web Crypto API(crypto.subtle)で Ed25519 署名検証を自前実装することでした。外部依存をゼロにできます。コードはこんな感じです。
async function verifyDiscordRequest(request: Request, body: string): Promise<boolean> {
const signature = request.headers.get('X-Signature-Ed25519') ?? '';
const timestamp = request.headers.get('X-Signature-Timestamp') ?? '';
const publicKey = await crypto.subtle.importKey(
'raw',
hexToUint8Array(PUBLIC_KEY),
{ name: 'Ed25519' },
false,
['verify']
);
const data = new TextEncoder().encode(timestamp + body);
const sig = hexToUint8Array(signature);
return crypto.subtle.verify('Ed25519', publicKey, sig, data);
}
標準API だけで完結するので、外部パッケージを増やさずに済みました。
つまづき②:公開Bot をオフにすると保存できない
自分専用Botなので、Discord Developer Portal の Bot 設定で「公開Bot」(英語UIでは PUBLIC BOT)をオフにしようとしました。ところが保存できません。
原因はインストールリンクが設定されたままだったことでした。左メニューの「インストール」ページを開いて、「インストールリンク」を「なし」に変更してから保存すると通ります。

先にリンクを外してから Bot ページに戻ると、今度はちゃんと保存できます。

つまづき③:スラッシュコマンドを登録したのに出てこない
/rumina コマンドを登録して Discord サーバーで試したのに、候補に出てきません。
原因はコマンドの integration_types 設定でした。ギルドインストール(サーバーへの追加)を有効化する前にコマンドを登録してしまったので、integration_types: [1](ユーザーインストール)だけで登録されていたんです。
Discord サーバーで使えるのは integration_types: [0](ギルドインストール)のほうです。以下のパラメータを追加して登録し直しました。
{
"integration_types": [0],
"contexts": [0]
}
integration_types と contexts の両方を正しく設定しないと、サーバーのスラッシュコマンド一覧に出てこないので気をつけてください。

つまづき④:RunPod ワーカーが起動しない
Workers から RunPod にリクエストを投げても、ワーカーが一向に起動しません。エラーすら返ってこないので原因が分かりにくいパターンです。
犯人は環境変数 RUNPOD_ENDPOINT_ID にエンドポイント名(ラベル)を入れていたことでした。API に渡すのは qbl13c0mqc5xvc のような英数字の羅列で、ダッシュボードに大きく表示されているラベル名(optimistic_maroon_planarian みたいなやつ)ではないんですね。
RunPod のダッシュボードでエンドポイントを開いて、URLや設定画面から正しいIDを確認して直しました。

コマンド受付からWebhook返却までの流れ(コードのイメージ)
// 入口: Discordコマンドを受けてRunPodへ投げる
if (url.pathname === '/') {
// 署名検証
// ...
// RunPodへ非同期ジョブを投げる(webhookに出口URLを指定)
await fetch(`https://api.runpod.ai/v2/${ENDPOINT_ID}/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${RUNPOD_API_KEY}`
},
body: JSON.stringify({
input: { workflow: comfyWorkflow },
webhook: `${WORKER_URL}/webhook`
})
});
// Discordには即座に「生成中...」を返す(3秒以内に応答が必要)
return new Response(JSON.stringify({ type: 5 }), { ... });
}
// 出口: RunPodのWebhookを受け取ってDiscordへ画像送信
if (url.pathname === '/webhook') {
const result = await request.json();
const imageBase64 = result.output.images[0].data;
const imageBytes = Uint8Array.from(atob(imageBase64), c => c.charCodeAt(0));
const formData = new FormData();
formData.append('files[0]', new Blob([imageBytes], { type: 'image/png' }), 'output.png');
await fetch(`https://discord.com/api/v10/webhooks/${APP_ID}/${interactionToken}`, {
method: 'POST',
body: formData,
});
}
この「即座に type: 5(Deferred Response)で応答してから、後でWebhookで画像を送る」という流れが、Discord の非同期対応パターンです。コマンド受信から3秒以内に何かレスポンスを返さないと Discord側がタイムアウトしてしまうので、処理を待たずに即返事するのがポイントですね。
type: 5(Deferred Response)で即答して、生成完了後に Followup Webhook で画像を送る構成が必須になります。type: 5 を返すと、ユーザー側にはこう見えます。Bot が何かメッセージを送っているわけではなくて、Discord が自動で「考え中…」の表示を出してくれるんです。

完成・動作確認
スマホの Discord アプリを開いて、Bot を追加したサーバーで /rumina と入力します。
コマンドのオプションで seed やスタイルを渡せるようにしました。送信するとすぐ「考え中…」の表示に変わって、コールドスタートなら4分半ほど待つとチャンネルに画像が届きます。

実際に使ってみると、BOOTH向けの素材確認がかなりラクになりました。「この角度どうかな?」という確認を外出先でできるのは、思っていた以上に作業効率に効いています。
かかったコスト・時間まとめ
| 項目 | コスト |
|---|---|
| GHCRストレージ | 現状無料(GitHub が課金開始する場合は1ヶ月前に予告あり) |
| RunPod | 生成中のみ課金。コールドスタート約4分30秒、ウォーム時は数秒 |
| Cloudflare Workers | 無料枠内(1日10万リクエスト) |
RunPodの料金が使った分だけなのは本当にありがたいです。BOOTH向けの確認作業くらいの頻度なら、月に数百円以下に収まると思います。
RunPodの課金の仕組みやクレジットの買い方は、別記事に詳しくまとめています。Serverlessはミリ秒単位の従量課金なので、Podの時間課金とは感覚がかなり違いますよ。

作業時間は以下の通りです。
| 工程 | 所要時間 |
|---|---|
| 工程①(GHCRイメージ作成) | 2〜3時間(Civitaiハマり含む) |
| 工程②(RunPodエンドポイント) | 1時間程度 |
| 工程③(Workers + Discord Bot) | 約3時間(つまづき多め) |
工程③が想定外に長かったですね。署名検証の自前実装、コマンド登録のやり直し、エンドポイントIDの混同と、立て続けにハマりました。
おわりに
最終的に「スマホから打ったら画像が返ってくる」という目的は達成できました。
今後やりたいことがいくつかあります。
プロンプトチューニング。今はWorkflowを固定で持たせているんですが、Discord コマンドのオプションでスタイルや構図を選べるようにしたいです。
LoRAの追加対応。checkpointと一緒にLoRAもイメージに焼き込んで、Discordで切り替えられる構成を検討中です。焼き込むLoRA自体もRunPod上で作っているので、その過程は別記事にまとめています。

ウォームアップの仕組み。コールドスタートの4分半がやっぱり惜しいです。ウォーム状態をキープする仕組みか、生成リクエストの前にウォームアップジョブを走らせる仕組みを入れたいところ。
全体としては、サーバー常駐なし・外部ストレージなし・常時課金なしで動く構成が作れました。同じように「自分専用のAI生成環境を持ち歩きたい」と思っている方の参考になればうれしいです。
よくある質問
read:packages スコープで発行してください。まとめ
- ComfyUI + checkpoint → GHCRイメージ焼き込み → RunPod Serverless の構成でサーバー不要
- Cloudflare Workers + Discord Bot でスマホから叩けるインターフェースを作れる
- 主なハマりポイントは「Civitai バージョンID」「GitHub Actions の小文字変換」「Web Crypto API での署名検証」「Discord integration_types の設定」「RunPod エンドポイントIDとラベルの混同」の5点
- コストは生成時のRunPod費用のみ。ストレージ・常駐サーバー代はかからない
RunPodはGPUを使った分だけ課金。月額固定費なしで試せるのが個人開発にちょうどいい。紹介リンクからGoogleアカウントで登録して$10以上チャージすると、$5〜$500のクレジットボーナスがもらえます
ボーナスの対象はGoogle登録のみ。手順は通常登録と同じです
⚠️ AI画像生成をご利用の際の重要な注意事項
著作権・知的財産権について
- 既存のキャラクター、作品、ブランドロゴなどの模倣・複製は著作権侵害にあたる可能性があります
- 商用利用時は特に注意が必要です
肖像権について
- 実在人物(著名人・一般人問わず)の顔や特徴を模倣した画像生成はお控えください
- 無断での肖像権使用は法的トラブルの原因となります
利用規約の確認
- 各AI画像生成サービスの利用規約を必ずご確認ください
- 商用利用の可否、生成画像の権利関係は各サービスで異なります
免責事項
- 当ブログの情報を参考にしたAI画像生成により生じた問題について、当ブログは一切の責任を負いません
- 法的問題が生じた場合は、利用者の自己責任となります
- 最新の法律・規約情報は公式情報をご確認ください
適切なAI画像生成を心がけ、創作活動を楽しみましょう。
詳細についてはAIと著作権についてをご覧ください。


コメント