Rork でアプリを作っていて「画像から何かを判定したい」「重めの推論モデルを動かしたい」となったとき、最初に直面するのが推論基盤の選択です。Workers AI は手軽ですが利用できるモデルが限定されます。一方で AWS や GCP に GPU インスタンスを立てるのは個人開発者には荷が重い。私はこの中間が長らく欲しいと思っていたのですが、2025 年に GA になった Cloudflare Containers でようやくその「ちょうどいい場所」が手に入りました。
ここではRork で生成した React Native コードから呼び出すことを前提に、Cloudflare Containers と Workers AI をどう使い分けるかを、私が実際に運用しているアプリの構成例とあわせてお話しします。
まず Workers AI で済ませられないかを疑う
Cloudflare Containers は便利ですが、まず最初に「本当に Containers が必要か」を考える価値があります。Workers AI で動くモデルがあるなら、ほぼ常にそちらの方が安く・速く・運用が楽です。
Workers AI で配信されている主なモデル系統には次のようなものがあります。
- 一般的な対話・要約系:Llama 系、Mistral 系、Qwen 系
- 画像生成:Flux、Stable Diffusion XL Lightning
- Embedding:BGE 系、E5 系
- 音声:Whisper(小サイズ)
たとえば「ユーザーが入力した日記を 3 行に要約する」「商品レビューの感情分類をする」程度なら、わざわざ Containers を使う理由はありません。Workers AI に投げて終わりです。
// app/api/summarize+api.ts (Rork が生成する Expo Router の API ルートに置く想定)
export async function POST(request: Request) {
const { text } = await request.json();
const ai = (request as any).env.AI; // Workers バインディング
const result = await ai.run("@cf/meta/llama-3.1-8b-instruct", {
messages: [
{ role: "system", content: "次の文章を日本語で3行に要約してください。" },
{ role: "user", content: text },
],
});
return Response.json({ summary: result.response });
}このコードはモバイル側からの fetch で完結します。コールドスタートはほぼゼロ、料金も無料枠が大きく、Rork から呼び出す分にはこれで十分まかなえるケースが大半です。
ではどんなときに Containers の出番が来るのでしょうか。
Containers を選ぶべき 3 つの局面
私が実装の中で「Workers AI では無理だ」と判断したのは、次の 3 パターンでした。
第一に、独自の学習済みモデルを動かす場合です。たとえば自分でファインチューニングした YOLOv8 を使った商品検出、特定のキャラクター画像を識別する CoreML→ONNX 変換モデルなど、Workers AI のカタログにないモデルは Containers に乗せるしかありません。
第二に、推論前後の重い前処理・後処理がある場合です。画像のリサイズ・正規化、PDF からのテーブル抽出、複数画像の合成といった処理は、Workers の CPU 時間制限(無料 10ms / 有料 30 秒)にひっかかりやすく、Containers の方が安全です。
第三に、長時間ジョブが必要な場合です。動画の文字起こし、長文 PDF の解析、ベクトル DB への一括投入など、数十秒〜数分かかる処理は Containers + Queues の組み合わせが本命になります。
逆に言えば、これらに当てはまらない場合は Containers は不要です。「なんとなく強そうだから」で導入すると、コールドスタートと固定費の両方を背負うことになります。
Rork アプリから Containers を呼び出す最小構成
実装の話に進みます。Rork で作ったアプリから Containers を叩く際、私は以下のような薄い Worker をフロントに置く構成を取っています。
[Rork App] → [Cloudflare Worker (認証・課金チェック・キュー投入)] → [Containers (推論)]
Worker をフロントに置く理由は 3 つあります。認証トークンの検証、Stripe 等の課金ステータスのチェック、そしてジョブの非同期化です。Containers を直接モバイルから叩くこともできますが、本番アプリでは課金や不正利用対策を間に挟みたいので、Worker をプロキシにします。
wrangler.toml の Containers 定義は次のようになります。
name = "rork-ai-backend"
main = "src/index.ts"
compatibility_date = "2026-04-01"
[[containers]]
name = "yolo-detector"
image = "./containers/yolo/Dockerfile"
instance_type = "standard-2" # 2 vCPU / 4GB
max_instances = 5
[[ai]]
binding = "AI"
[[queues.producers]]
binding = "DETECT_QUEUE"
queue = "yolo-detect"ポイントは instance_type の選び方です。GPU が必要な場合は gpu-* 系を選びますが、単価が大きく上がるので、まずは CPU 系で動くかを試すのを強くおすすめします。私のケースでは ONNX Runtime に変換することで CPU 系インスタンスで十分実用速度が出ました。
Worker 側のエントリポイントは次のようになります。
// src/index.ts
import { Container } from "@cloudflare/containers";
export class YoloDetector extends Container {
defaultPort = 8080;
sleepAfter = "30s"; // アイドル時間でスケールダウン
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/api/detect" && request.method === "POST") {
// 認証
const userId = await verifyAuth(request, env);
if (!userId) return new Response("Unauthorized", { status: 401 });
// 課金プランの確認(無料枠超過チェック)
const allowed = await checkQuota(userId, env);
if (!allowed) return new Response("Quota exceeded", { status: 402 });
// Container にプロキシ
const id = env.YOLO_DETECTOR.idFromName("singleton");
const container = env.YOLO_DETECTOR.get(id);
return container.fetch(request);
}
return new Response("Not Found", { status: 404 });
},
};sleepAfter = "30s" はコスト最適化の要です。アクセスが途切れた 30 秒後にコンテナが停止し、課金が止まります。次のリクエストではコールドスタートが発生しますが、私のアプリでは 2〜3 秒で立ち上がっています。リアルタイム性が必要な機能では sleepAfter を長めに設定するか、定期的に温めるリクエストを送る運用が必要です。
モバイル側(Rork 生成コード)からの呼び出し
Rork が生成するコードは React Native + Expo です。fetch 呼び出しは以下のように、認証トークンと共に送ります。
// app/(tabs)/detect.tsx の中で使うフック想定
async function detectObjects(imageUri: string, token: string) {
const formData = new FormData();
formData.append("image", {
uri: imageUri,
name: "photo.jpg",
type: "image/jpeg",
} as any);
const res = await fetch("https://rork-ai-backend.example.workers.dev/api/detect", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (res.status === 402) {
throw new Error("PLAN_REQUIRED");
}
if (!res.ok) {
throw new Error(`Detection failed: ${res.status}`);
}
return res.json() as Promise<{ objects: Array<{ label: string; score: number }> }>;
}PLAN_REQUIRED を別エラーで投げているのは、UI 側でメンバーシップ誘導モーダルを出し分けるためです。私はこの 402 を受けて Rork で作ったアプリの収益化完全ガイド で書いたサブスク導線につなげる設計にしています。
コスト感覚:実際にいくらかかるか
私が運用している小規模アプリ(DAU 約 200、推論回数月 1.5 万回)での月額は以下のような内訳でした。
- Workers リクエスト:無料枠内
- Containers 稼働時間:約 $4.20(standard-2、ピーク時のみ稼働)
- R2 保存(推論前後の画像):約 $0.30
- 合計:月 $5 弱
これは GPU を使わず、ONNX に変換した CPU 推論で済ませた場合の数字です。GPU インスタンスを使うとざっくり 5〜10 倍程度になりますので、本当に GPU が必要かは慎重に検証してください。
ちなみに同じ処理を AWS の g4dn.xlarge で常時稼働させた場合は月 $300 を超えるので、個人開発者にとって Containers のスケール・トゥ・ゼロは決定的なコスト優位性になります。バックエンド設計をもう一段深掘りしたい場合は Rork × Hono × Cloudflare Workers REST API 実装ガイド も参考になるはずです。
運用してみて分かった落とし穴
最後に、実際に本番運用する中でつまずいたポイントを 3 つだけ共有します。
ひとつ目は、コールドスタート中に来たリクエストの扱いです。Containers が停止状態から起動するまで、後続リクエストは待たされます。タイムアウトを Worker 側で 30 秒に設定しておかないと、モバイル側のデフォルトタイムアウト(多くの fetch ライブラリで 60 秒前後)が先に切れて、ユーザー側はエラーに見えるのに裏では推論が完了しているという状態になります。
ふたつ目は、ログの集約です。Containers のログはデフォルトでは Worker の console.log と別系統になります。私は Sentry を Containers 内のアプリにも仕込んで、Worker と Containers のエラーを同じダッシュボードで見られるようにしました。
みっつ目は、デプロイのアトミック性です。Worker の更新と Containers のイメージ更新は別操作になるため、新しい API スキーマを期待する Worker が古いイメージにつながる瞬間が発生します。私はこれをスキーマバージョンをパスに含めることで対処しています(/api/v2/detect のような形)。
さいごに
Cloudflare Containers は「Workers AI では足りない、でも GPU インスタンスを建てるほどではない」という個人開発者の隙間ニーズにぴたりとはまるサービスです。Rork で生成したアプリのバックエンドとしては、まず Workers AI で動かしてみて、限界を感じた機能だけを Containers に切り出すのが最もコスト効率の良いアプローチだと感じています。
いきなり全機能を Containers に乗せるのではなく、まずは画像分類のような限定機能でひとつ動かしてみてください。wrangler containers deploy を一度叩くだけで、自分のアプリが「AI バックエンドを持つ本格的なサービス」に化ける感覚は、一度体験するとやめられないと思います。
Workers の挙動を理解しておくと、Containers との境界設計がぐっと楽になります。