Google Cloud Run¶
GCP Hono Validation Container
Hono を中核に据えたコンテナファーストの ReScript サービスで、Google Cloud Run へのデプロイをすぐに行える。Hono (Node.js) スターターと異なり、このテンプレートは設計済みのマルチステージ Dockerfile、.dockerignore、.env.example、そして gcloud run deploy と Cloud SQL のレシピを順を追って説明する README セクションを同梱している。
ランタイムイメージは意図的に小さく構成されている。ビルダーステージで全依存関係をインストールし ReScript をコンパイルした後、ランタイムステージで 本番用のみ の依存関係を再インストールし、生成された .res.mjs ファイルをコピーし、非 root ユーザーにドロップしてポート 8080 を公開する。結果として、ローカル・CI レジストリ・Cloud Run のいずれの環境でも同じ動作をする決定的なイメージが得られる。
生成内容¶
my-service/
├── rescript.json
├── package.json # ESM, "type":"module", engines.node = >=24
├── Dockerfile # multi-stage: builder → runtime
├── .dockerignore # excludes node_modules, lib/, coverage, .env
├── .env.example # PORT=8080 (Cloud Run sets it for you)
├── src/
│ ├── ServerMain.res # entry — calls Server.start()
│ ├── Server.res # Hono app: GET /, POST /echo, reads PORT
│ ├── Hono.res # bindings shared with other Hono templates
│ ├── HonoNodeServer.res # @hono/node-server bindings
│ ├── Validation.res # zod or sury — selected in the wizard
│ └── __tests__/Server.test.mjs # vitest smoke test that imports Server.res.mjs
├── README.md # script docs + API + Environment + Deploy + Cloud SQL
├── LICENSE # MIT, holder = project name
├── .nvmrc # Node 24
├── .gitignore # node_modules + dist/ + .env + ReScript artifacts
├── .editorconfig # 2-space indent, LF line endings
└── .github/
├── dependabot.yml # weekly npm updates
└── workflows/ci.yml # install + rescript build + vitest
ウィザードオプション¶
オプション |
効果 |
|---|---|
Project name |
npm の |
Package manager |
npm / pnpm / yarn / bun のいずれか。 |
Validation library |
|
Dockerfile の分岐は、他の Hono テンプレートとの意味のある違いとなっている。Bun を選ぶとベースイメージは oven/bun:1-slim に切り替わり、すべての npm install / pnpm install 行は bun install に置き換わり、ランタイムユーザーは node から bun に変わる(どちらも uid 1000)。Node のメジャーバージョンを nodeMajor (デフォルトは TemplateVersions.NODE_MAJOR)に固定することで、gcloud run deploy とローカルの node --watch が常に同じランタイムを見るようになる。package.json の engines とイメージのバージョンずれは発生しない。
主要な依存¶
パッケージ |
用途 |
バージョン |
|---|---|---|
|
ReScript コンパイラ |
|
|
標準ライブラリ |
|
|
コンパイル済み |
|
|
HTTP フレームワーク。小型・高速で、Web 標準の |
|
|
Hono アプリを Node の HTTP サーバーにマウントするアダプタ |
|
|
バリデーションバックエンド(ウィザードで選択) |
|
|
スモークテストランナー |
|
|
|
|
主要なファイル¶
src/Server.res¶
Hono アプリ本体。process.env から PORT を読み取り(Cloud Run が自動的に設定する)、ローカル開発用には 8080 にフォールバックする:
@val external processEnv: Dict.t<string> = "process.env"
let port =
processEnv
->Dict.get("PORT")
->Option.flatMap(Int.fromString(_))
->Option.getOr(8080)
let app = Hono.createApp()
app->Hono.get("/", ctx => ctx->Hono.text("Cloud Run + Hono + ReScript"))
app->Hono.post("/echo", async ctx => {
let raw = await ctx->Hono.req->Hono.jsonBody
switch Validation.parseEchoPayload(raw) {
| Error(msg) => ctx->Hono.status(400)->Hono.json({"error": msg})
| Ok(payload) =>
ctx->Hono.json({"echo": payload.message, "receivedAt": Date.now()->Float.toString})
}
})
let start = () => {
HonoNodeServer.serve({fetch: app->HonoNodeServer.honoFetch, port})
Console.log(`Server running on http://localhost:${port->Int.toString}`)
}
CORS フックはファイル冒頭付近にコメントアウトされた状態で残されている。別オリジンのブラウザからサービスを呼び出す場合はコメントを解除し、デプロイ前に許可リストを調整すること。
src/ServerMain.res¶
中身は Server.start() の 1 行のみ。この分割があることで、vitest が import("../Server.res.mjs") を実行してポートにバインドせずにアプリのコンパイルを検証できる。
src/Validation.res¶
parseEchoPayload: JSON.t => result<echoPayload, string> —— /echo ルートのボディに対するランタイムコントラクト。シグネチャは zod / sury のどちらのバリアントでも同一なので、呼び出し側がどちらのライブラリが採用されたかで分岐する必要はない。
Dockerfile¶
ウィザード入力から生成される 2 ステージのイメージ。具体的な内容は選択したパッケージマネージャと nodeMajor に依存するが、形は次のとおり:
# syntax=docker/dockerfile:1.7
# --- Builder stage: install all deps + compile ReScript ---
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN <pm-install> # e.g. corepack enable && pnpm install --frozen-lockfile=false
COPY . .
RUN <pm-exec> rescript # e.g. pnpm exec rescript / npx rescript / bunx rescript
# --- Runtime stage: prod deps + compiled output only ---
FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN <pm-install-prod> # e.g. pnpm install --prod --frozen-lockfile=false
COPY --from=builder /app/src ./src
USER node
EXPOSE 8080
CMD ["node", "src/ServerMain.res.mjs"]
次の 3 つの性質は意図的であり、設計上重要である:
node_modulesはステージ間でコピーされない ——--prodフラグで再インストールされるため、ランタイムレイヤーはロックファイルから再現可能となる。ランタイムステージは非 root ユーザーで動作する(Node イメージでは
node、oven/bunではbun)。Cloud Run の堅牢化ポリシーがこれを前提としている。ベースイメージの Node メジャーバージョンは
package.jsonのengines.nodeおよび.nvmrcと一致する。nodeMajorを更新すると、3 つすべてが同時に更新される。
.dockerignore¶
VCS メタデータ、IDE の状態、パッケージマネージャのデバッグログ、lib/ の ReScript ビルド成果物、coverage/、すべての .env* ファイルを除外することでビルドコンテキストを小さく保つ。!.env.example 行はサンプル env ファイルを保持し、README のデプロイ手順が引き続きそれを参照できるようにする。
npm スクリプト¶
スクリプト |
説明 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ローカル開発では res:dev と dev を 2 つのターミナルで動かす(あるいは、シングルウィンドウのワークフローを好むなら、自前の concurrently 呼び出しで連結する)。
Cloud Run へのデプロイ¶
README には、Cloud Build でイメージをプッシュし gcloud run deploy でロールアウトするコピー&ペースト可能なデプロイブロックが含まれている:
gcloud builds submit --tag gcr.io/PROJECT-ID/<projectName>
gcloud run deploy <projectName> \
--image gcr.io/PROJECT-ID/<projectName> \
--port 8080 \
--allow-unauthenticated
Cloud Run はランタイムで PORT を注入する(値が常に 8080 とは限らない)。そのため Server.res は process.env からこの値を読み取る。--port 8080 フラグはコンテナがリッスンするポートを Cloud Run に伝えるものであり、公開するポートを指定するものではない —— これらは独立した設定である。
README ではさらに 2 つの運用フォローアップを扱っている。デプロイ済みリビジョンのイメージダイジェストを固定して不変ロールアウトを実現する方法と、gcloud run services logs tail によるログのテーリングである。
Cloud SQL レシピ¶
同梱の README には、@google-cloud/cloud-sql-connector(またはバニラの pg ドライバと Cloud SQL Auth Proxy の組み合わせ)を使って Cloud SQL の Postgres インスタンスに接続する短いレシピが含まれている。要約すると次のパターンになる:
pnpm add pg @google-cloud/cloud-sql-connector
@module("pg") @new external makePool: 'opts => 'pool = "Pool"
@send external queryAsync: ('pool, string, array<'a>) => promise<'rows> = "query"
gcloud run deploy の --set-env-vars を使い、DATABASE_URL(または Cloud SQL 接続名と認証情報)を環境変数として渡すこと。Postgres 上で Drizzle ORM を使う場合は Drizzle のセットアップ レシピを参照する。libsql クライアントを pg に置き換えれば同じ構造がそのまま当てはまる。
2日目以降のレシピ¶
Hono エンドポイントの追加 ——
/echoと並ぶ型付き POST/GET ルートを追加するDrizzle のセットアップ —— Drizzle ORM のスキーマを追加する(Cloud SQL 向けには libsql の例を
pgに置き換えて使う)OpenAPI ドキュメントの追加 ——
@hono/zod-openapiのスキーマから Scalar API ドキュメントを生成する
プロジェクトを開いた後の ReScript 側のエディタワークフローについては 機能概要 を参照する。
補足¶
PORTはマニフェストで設定できない。 Cloud Run が呼び出しごとに設定し、Server.resがそれを尊重する。ローカルではPORT=3000 pnpm devが動作する。これは同じ env 読み取りロジックが、8080 にデフォルトする前にユーザー入力にフォールバックするためである。Dockerfile はロックファイルがない場合でも
bun install --productionを再実行する。 Bun は初回インストール時にbun.lockを作成する。コミット済みロックファイルがない CI 環境でも動作するイメージは生成されるが、再現性を保証するため本番デプロイでは必ずロックファイルをコミットすべきである。COPY --from=builder /app/src ./srcはステージ間で受け渡される唯一の成果物である。追加の出力(dist/、public/)を生成する場合は、それぞれに対して明示的なCOPY --from=builder行を追加する必要がある。nodeエンジンは>=24に固定されており、.nvmrcは24を指定する。Dockerfile のベースイメージはnodeMajor(デフォルトはTemplateVersions.NODE_MAJOR)に追従する。ウィザードでいずれかを変更するとすべてが更新される。vitest スイートは意図的に最小限である ——
import("../Server.res.mjs")が解決することのみを検証する。リグレッションカバレッジをテストに依存させる前に、app.request("/")スタイルの統合テスト(例は Monorepo および Full-Stack テンプレートを参照)を追加すること。.gitignoreは.envを除外する が、.env.exampleは保持する。Cloud Run のシークレットは--set-env-varsか Secret Manager を経由して渡すべきで、コミットした.env経由で渡してはならない。CORS はデフォルトで無効になっている。 別オリジンのブラウザサイドクライアントにサービスを公開する前に、
Server.res冒頭のHono.cors(...)ブロックのコメントを解除し、オリジンの許可リストを固定すること。