空雲 Blog

Eye catch
Deno Deploy がサ終するので代替サービスを探した結果、Rivetに行き着いた

publication: 2026/11/08
update:2026/11/08

はじめに🔗


Deno Deploy のサービス終了のアナウンスを受け、エッジやサーバーレス環境で動かしていた WebAssembly(WASM)ツール(画像最適化や動的 OGP 生成など)の移行先を探していました。選定条件としては、低コストかつ商用利用が可能で、メモリ制限が厳しくないことです。安価な選択肢としては Cloudflare Workers が筆頭に挙がりますが、メモリ上限が 128MB に制限されているため、画像処理やフォントレンダリングを伴う用途にはやや厳しい面があります。


そこで有力な候補として見つけたのが Rivet です。無料プランでありながら 2GB のメモリを利用できます。本記事では、Rivet の特徴や無料枠の実際の仕様をはじめ、WASM(wasm-html-to-image)を用いた画像最適化と動的 OGP 生成(Hono JSX + Mermaid + 縦書き対応)を Rivet アクターとして実装・デプロイした知見をまとめます。




Rivet とは🔗


Rivet(RivetKit)は、ステートフルなアクターモデル(Actor Model)を TypeScript / Web 標準スタックで構築できるプラットフォームです。


従来の一般的なサーバーレス(AWS Lambda や Cloudflare Workers)と比べると、次のような強みがあります。


  1. アクターごとに独立した組み込み SQLite(Durable State)
    • アクターごとに専用の SQLite データベースが自動プロビジョニングされます。
    • 外部の KV や Redis、データベースを個別に契約しなくても、プロセスローカルな超低レイテンシでキャッシュや状態管理が行えます。
  2. Dockerfile ベースの柔軟なコンテナ実行
    • 制限の多いエッジランタイム(V8 isolate のみ)とは異なり、コンテナベースで動作するため、Node.js のネイティブモジュールや WebAssembly(WASM)バイナリが制約なく動作します。
  3. HTTP ゲートウェイと型安全な RPC
    • 通常の Web リクエストを受ける低レベル HTTP ハンドラ(onRequest)と、クライアント SDK から直接型安全に呼び出せる RPC メソッド(actions)の両方を 1 つのアクターで定義できます。
  4. 自動スリープ & 即時復帰
    • リクエストがないときはメモリを解放して自動的にスリープし、リクエストが届くとミリ秒単位で即座に復帰します。




Rivet の無料枠とリソース仕様🔗


Rivet Cloud(Free プラン)の仕様を API や実機メトリクスから調査したところ、一般的なサーバーレスとは異なる特徴的な枠設計になっていました。


1. 「CPU 時間の合算上限」ではなく「ノード上限型」🔗

一般的な FaaS(月あたり 100,000 秒まで等の CPU 時間クォータ制)とは異なり、Rivet の無料プランは「1 ノード(最大 0.5 vCPU / 2GB RAM)が常時割り当て可能」という仕様になっています。


  • ランナーの課金フラグ (runners.billed): false(無料)
  • 最大ノード数 (max_nodes): 1 台
  • 利用可能マシンタイプ:
    • R-5(e2-micro: 0.25 vCPU, 1 GB RAM)
    • R-10(e2-small: 0.5 vCPU, 2 GB RAM)※Free プランで利用可能
  • ストレージ: 4 GB
  • 転送量 (Egress): 100 GB / 月


2. CPU を使い続けた場合の挙動🔗

Free プランで割り当てられる e2-small(0.5 vCPU)は GCP の共有コアインスタンスです。

  • CPU 時間による従量課金が勝手に発生することはありません。
  • ただし、常時 100% の負荷をかけ続けると、クラウド基盤側で CPU スロットリング(一時的な速度制限)が発生します。
  • 画像変換や OGP 生成のように「アクセスが来た瞬間にサッと処理し、終わったらアイドル/スリープする」用途であれば、リソース使用率は数%台で非常に快適に推移します。


3. プロジェクト作成時の設定🔗


基本的には無料枠で十分動作するため Free プランを選択します。リージョンは日本から最も地理的に近い asia-east1(台湾)を選ぶのがおすすめです。
また、Actor Nodes の設定で R-10 を選択すると、無料枠の範囲内のままスペックを 0.5 vCPU / 2GB RAM に引き上げられます。


{"width":"842px","height":"1458px"}




実際に作ったもの🔗


今回は以下の 2 つのサービスを Rivet アクターとして実装しました。


  1. rivet-image-optimization: URL 指定された画像を取得し、WASM で AVIF / WebP / JPEG にリサイズ・軽量化して SQLite に永続キャッシュするアクター。
  2. rivet-ogp: Hono JSX で 1200×630 のリッチなカードを構築し、UnoCSS / Tailwind の動的 CSS 生成、Mermaid ダイアグラム描画、CSS 縦書きバッジを組み合わせて WASM で PNG レンダリングする OGP 生成アクター。


ちなみに画像変換の実験では、6500*6500の画像出力に成功しました。4K画像レベルなら余裕です。




実装コードの解説🔗


1. アクター定義と SQLite キャッシュ🔗


RivetKit では actor 関数の中で db オプションを指定するだけで、マイグレーション付きの SQLite が利用できます。


1import { actor, setup } from "rivetkit";
2import { db } from "rivetkit/db";
3import { render } from "wasm-html-to-image";
4import { createCSS } from "wasm-html-to-image/tailwind";
5
6export const IMAGE_CACHE_SCHEMA = `
7 CREATE TABLE IF NOT EXISTS image_cache (
8 key TEXT PRIMARY KEY,
9 content_type TEXT NOT NULL,
10 data BLOB NOT NULL,
11 created_at INTEGER NOT NULL
12 );
13 CREATE INDEX IF NOT EXISTS idx_image_cache_created_at ON image_cache(created_at);
14`;
15
16export const ogpGenerator = actor({
17 // アクター専用の SQLite データベース
18 db: db({
19 onMigrate: async (database) => {
20 await database.execute(IMAGE_CACHE_SCHEMA);
21 },
22 }),
23
24 // 生の HTTP リクエストを受け取るハンドラ
25 onRequest: handleOgpRequest,
26
27 // クライアントから呼べる型安全な RPC アクション
28 actions: {
29 async generate(c, params: OgpOptions) {
30 return generateOgpImage(c.db, params);
31 },
32 },
33});
34
35export const registry = setup({
36 use: {
37 ogpGenerator,
38 },
39});
40
41// Rivet Cloud ランナー環境で自動起動
42if (process.env.RIVET_ENDPOINT || process.env.RIVET_RUNNER) {
43 registry.start();
44}


2. キャッシュ判定と WASM レンダリング🔗


リクエストが来たら、まず SQLite の image_cache を検索します。ヒットすればそのままバイナリを返し、ヘッダーに X-Cache: HIT を付与します。キャッシュがない場合のみ、HTML を構築して WASM エンジンでレンダリングします。


1export async function generateOgpImage(
2 database: CacheDatabase,
3 options: OgpOptions,
4): Promise<OgpResult> {
5 const cacheKey = buildCacheKey(options);
6
7 // 1. SQLite キャッシュをチェック
8 const rows = await database.execute<CacheRow>(
9 "SELECT content_type, data FROM image_cache WHERE key = ?",
10 cacheKey,
11 );
12 if (rows[0]) {
13 return {
14 contentType: rows[0].content_type,
15 data: new Uint8Array(rows[0].data),
16 cached: true,
17 };
18 }
19
20 // 2. Hono JSX による HTML 構築と UnoCSS のオンデマンド CSS 生成
21 const html = renderOgpTemplate(options);
22 const css = await createCSS(html);
23
24 // 3. wasm-html-to-image による PNG レンダリング(Headless Chrome 不要)
25 const rendered = await render({
26 value: html,
27 css,
28 width: 1200,
29 height: 630,
30 format: "png",
31 });
32
33 const responseData =
34 typeof rendered.data === "string"
35 ? new TextEncoder().encode(rendered.data)
36 : new Uint8Array(rendered.data);
37
38 // 4. SQLite にキャッシュを保存
39 await database.execute(
40 "INSERT OR REPLACE INTO image_cache (key, content_type, data, created_at) VALUES (?, ?, ?, ?)",
41 cacheKey,
42 "image/png",
43 responseData,
44 Date.now(),
45 );
46
47 return {
48 contentType: "image/png",
49 data: responseData,
50 cached: false,
51 };
52}


3. Hono JSX によるリッチな OGP テンプレート🔗


Headless Chrome を起動せず、WASM のみで高品位な OGP 画像を描画します。Mermaid ダイアグラムの埋め込みや CSS 縦書き(writing-mode: vertical-rl)もそのまま記述できます。


1export function renderOgpTemplate(options: OgpOptions): string {
2 const { title = "Title", subtitle = "subtitle", vertical = "縦書き対応" } = options;
3
4 return (
5 <html className="m-0 p-0">
6 <head>
7 <link
8 href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap"
9 rel="stylesheet"
10 />
11 <style>{`
12 .vertical-pill {
13 writing-mode: vertical-rl;
14 text-orientation: upright;
15 letter-spacing: 0.35em;
16 font-family: 'Noto Sans JP', sans-serif;
17 }
18 .mermaid {
19 --mermaid-theme: dark;
20 color: #cdd6f4;
21 }
22 `}</style>
23 </head>
24 <body className="m-0 p-0">
25 <div className="w-[1200px] h-[630px] flex relative bg-[#0a0a0c] overflow-hidden">
26 {/* 左カラム: タイトルとサブタイトル */}
27 <div className="flex flex-col w-[530px] h-full p-[60px] justify-between z-10">
28 <div>
29 <div className="text-[68px] font-black text-white leading-[1.1] mb-[24px]">
30 {title}
31 </div>
32 <div className="text-[28px] text-[#94a3b8]">{subtitle}</div>
33 </div>
34 <div className="text-sm text-slate-400">Powered by Rivet</div>
35 </div>
36
37 {/* 中央: 縦書きアクセントピル */}
38 <div className="absolute top-[60px] left-[570px] z-20">
39 <div className="vertical-pill text-[13px] font-bold text-[#c7d2fe] border border-indigo-400/40 p-4 rounded-3xl backdrop-blur-md">
40 {vertical}
41 </div>
42 </div>
43
44 {/* 右カラム: Mermaid ダイアグラムウィンドウ */}
45 <div className="relative flex-1 h-full z-10 p-10 flex items-center justify-center">
46 <div className="w-[500px] h-[260px] rounded-2xl border border-white/20 p-4 bg-white/5 backdrop-blur-md">
47 <div className="mermaid">
48 {options.mermaid ?? "flowchart LR\nClient --> Rivet[Rivet Actor] --> Image"}
49 </div>
50 </div>
51 </div>
52 </div>
53 </body>
54 </html>
55 ).toString();
56}




デプロイと運用のポイント・注意点🔗


実際にデプロイするにあたって、いくつか重要な注意点やハマりどころがありました。


1. 公式 CLI のパッケージ名は rivet ではなく @rivetkit/cli🔗

ドキュメントでは rivet deploy のように rivet コマンドで説明されていることが多いですが、npm でそのまま npm install -g rivet や npx rivet を実行すると、同名で登録されている別物の古いパッケージ(v1.0.7)がインストールされてしまいます。当然 deploy コマンドなどは存在せずエラーになります。


公式の Rivet CLI の正しいパッケージ名は @rivetkit/cli です。


1# 誤り: 別の古いパッケージが入る
2npm install -g rivet
3
4# 正しい: 公式の Rivet CLI
5npm install -g @rivetkit/cli
6# または
7npx @rivetkit/cli deploy


2. Windows ネイティブ環境には未対応(WSL / Linux が必須)🔗

現在、Rivet CLI(@rivetkit/cli)のバイナリパッケージは Linux(x64 / arm64)および macOS(x64 / arm64)のみ提供されています。


Windows の PowerShell やコマンドプロンプトでそのまま実行しようとすると、以下のようなエラーが発生します。


1Error: Windows x64 is not yet supported via npm platform packages


そのため、Windows 環境で開発・デプロイを行う場合は、WSL(Windows Subsystem for Linux)上の Node.js 環境から CLI を実行する必要があります。


1# WSL 側で CLI を実行
2wsl bash -ic "npx @rivetkit/cli deploy"


3. マルチステージビルドによるイメージ削減🔗

Rivet へのデプロイは Dockerfile を使って行われます。開発依存パッケージを含めたまま素直にビルドするとイメージサイズが 1GB を超えてしまい、デプロイや起動に時間がかかってしまいます。
以下のようにマルチステージビルドを組むことで、イメージサイズを約 500MB 程度まで軽量化できます。


1# --- 1. ビルドステージ ---
2FROM node:22-bookworm-slim AS builder
3WORKDIR /app
4COPY package.json tsconfig.json ./
5RUN npm install
6COPY src ./src
7RUN npm run build
8
9# --- 2. 実行ステージ(スリム化) ---
10FROM node:22-bookworm-slim AS runner
11WORKDIR /app
12ENV NODE_ENV=production
13
14COPY package.json ./
15RUN npm install --omit=dev && npm cache clean --force
16
17COPY assets ./assets
18COPY --from=builder /app/dist ./dist
19
20ENV PORT=3000
21EXPOSE 3000
22
23CMD ["node", "dist/server.js"]


4. デプロイ時のクラスタ上書きに注意🔗

Rivet では、rivet deploy はクラスタ単位で反映されます。
複数のサービスを運用したい場合、同一のクラスタ(例: "cluster": "cloud")に対して別プロジェクトからデプロイを実行すると、既存のビルドが新しいプロジェクトのイメージで上書きされてしまいます。


複数の機能を共存させたい場合は、以下のいずれかのアプローチを取るのがおすすめです。


  1. マルチアクター構成(推奨):
    1 つのコンテナ内に両方のアクターを含め、setup({ use: { imageOptimizer, ogpGenerator } }) としてまとめてデプロイします。Free プランの 1 ノードを効率的に共用できます。
  2. プロジェクト / クラスタを分離する:
    ダッシュボードからプロジェクトごとに新しいクラスタを作成し、別々のエンドポイントとしてデプロイします。




まとめ🔗


Deno Deploy の移行先を探す中で見つけた Rivet ですが、実際に動かしてみて特に次のように感じました。


  • 組み込み SQLite がアクターと一体化しており、キャッシュや永続化の設計が非常にシンプル
  • Dockerfile によるコンテナの自由度を持ちながら、サーバーレスのように自動スリープ・即時復帰が可能
  • 無料プランでも 0.5 vCPU / 2GB RAM の実用的なスペックが確保されており、WASM などのメモリを消費する処理も快適に動作する


ステートフルなバックエンドや、WASM・ネイティブバイナリを伴うサーバーレス構成を検討している方は、ぜひ一度 Rivet を試してみてはいかがでしょうか。