Cloud Run入門:gcloud run deployでコンテナを公開し、0までスケールさせる
ExpressのDockerイメージをCloud Runへ。PORT待受の必須ルール、gcloud run deployの手順、0スケールと料金、Cloud Buildでの自動ビルドを、コピペで動くコード付きで説明します。
「このAPI、どこに置けば一番ラクなんだろう」
個人開発で書いたExpressのWebhook受信サーバー。常時動かすほどではないのに、置き場所だけで毎月の請求が地味に痛い。EC2を立てれば寝てる間も課金され、Kubernetesは明らかにオーバーキル。Lambdaに寄せようとしたら、ネイティブ依存のビルドでハマって半日溶かしました。
最終的にたどり着いたのが Cloud Run でした。Dockerイメージをそのまま投げると、リクエストが来たときだけコンテナが起きて、来ない夜中は0台まで縮む。つまり誰も叩かない時間は1円もかからない。この「触らなければ無料に近い」感覚は、一度味わうと戻れません。
ただ、最初のデプロイで一発ハマりました。コンテナがいつまで経っても起動完了にならない。原因は「待ち受けるポート」を間違えていただけ。Cloud Runには独特のお作法がいくつかあって、そこさえ押さえれば驚くほど素直です。今日はその勘所を、僕の失敗込みで全部書きます。
この記事の要点
- Cloud Runは「Dockerイメージを渡すと、HTTPリクエストが来たときだけ起動するGCPのサーバーレス基盤」。普通のExpress/FastifyアプリをそのままコンテナでOK。
- コンテナの絶対ルールは環境変数
PORTで渡されたポートを待ち受けること。8080固定で書くと起動失敗する。 - デプロイは
gcloud run deploy --source .の一発でいける。手元にDockerが無くてもCloud Buildが裏でビルドしてくれる。 --min-instances 0で誰も来ない時は0台。リクエスト課金なら待機中は無料(毎月200万リクエスト等の無料枠つき)。コールドスタートが嫌なら1以上に。- 料金はvCPU秒・メモリGB秒・リクエスト数の3軸。
--cpu/--memory/--concurrencyの3つで使い心地とコストが決まる。
最初に用語だけ平たくしておきます。Cloud Runは「コンテナの実行係」、Artifact Registryは「Dockerイメージの倉庫」、Cloud Buildは「ソースからイメージを焼く工場」、Secret Managerは「パスワードの金庫」です。これだけ頭に入れれば、以下はすらすら読めます。
Cloud Runが向いている仕事
Cloud Runは万能ではありません。数ミリ秒で終わる単純なイベント処理だけなら、Cloud FunctionsやLambdaの方が軽いこともある。逆に「普通のWebアプリ/APIをコンテナごと持ち込みたい」なら、ここが一番ストレスが少ないです。
| やりたいこと | Cloud Runが向く理由 |
|---|---|
| Webhook受信API | Stripe・GitHub・LINEのPOSTをExpressで素直に受けられる |
| 小さなBFF / APIサーバー | ルーティング・認証・ミドルウェアを普通のNodeアプリとして書ける |
| バッチ起動用のHTTP口 | Cloud SchedulerからHTTPで叩いてジョブ化できる |
| LLMの軽い推論API | ネイティブ依存やバイナリをコンテナに閉じ込められる |
判断はシンプルです。**「Dockerfileが書けるWebアプリかどうか」**で考える。書けるなら、まずCloud Runを試して損はありません。常駐ワーカーやGPUをガン回しする推論基盤など、HTTPの形に収まらない処理だけ別を検討します。
ここがAWSやCloudflareとの住み分けでもあります。ECS/Fargateはクラスタやタスク定義など覚えることが多く、その分だけ自由度も高い(ECS/Fargateの設計はこちらで別途まとめています)。Cloudflare Workersはエッジで爆速ですが、Nodeの全機能は使えず書き換えが要る(Cloudflare Workers入門参照)。Cloud Runは、手元のDockerイメージをほぼそのまま、設定少なめで公開できるのが立ち位置です。
まず手元で動くExpressを用意する
Cloud Run最大のお作法から入ります。コンテナは、環境変数PORTで渡されたポートを待ち受けないといけません。Cloud Runは起動時にPORT(デフォルト8080)を注入してくるので、ここを固定値で書くとリクエストが届かず、起動チェックに失敗します。僕が最初にハマったのは、まさにこれでした。
正しくはprocess.env.PORTを読みます。/healthは後でデプロイ確認に使う疎通用エンドポイントです。
import express from "express";
const app = express();
app.use(express.json());
// 起動時に必須の環境変数を検証する(無ければ即終了)
const requiredEnv = ["DATABASE_URL", "JWT_SECRET"];
for (const key of requiredEnv) {
if (!process.env[key]) {
console.error(`必須の環境変数がありません: ${key}`);
process.exit(1);
}
}
app.get("/health", (_req, res) => {
res.status(200).json({ ok: true, service: "myapp-api" });
});
app.post("/webhooks/example", (req, res) => {
console.log("webhook_received", {
eventType: req.body?.type ?? "unknown",
receivedAt: new Date().toISOString(),
});
res.status(202).json({ accepted: true });
});
// ここが肝。8080固定ではなく、Cloud Runが渡すPORTを読む
const port = Number(process.env.PORT ?? 8080);
const server = app.listen(port, () => {
console.log(`listening on ${port}`);
});
// Cloud Runは停止時にSIGTERMを送る。処理中のリクエストを捌いてから閉じる
process.on("SIGTERM", () => {
console.log("SIGTERM received, closing HTTP server");
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 30000).unref();
});
package.jsonとtsconfig.jsonは素のTypeScript構成で十分です。
{
"name": "cloud-run-claude-code-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"express": "^4.19.2"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.10.0",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}
ローカルでは、Secret Managerを使う前でもダミー値を渡せば起動します。
npm install
DATABASE_URL="postgresql://local" JWT_SECRET="local-secret" npm run dev
curl http://localhost:8080/health
{"ok":true,...}が返れば、Cloud Runに乗せる準備の半分は終わりです。
いきなりデプロイ:gcloud run deploy 一発
ここがCloud Runの一番気持ちいいところ。Dockerfileを書かなくても、ソースを丸ごと渡せばデプロイできます。--source .を付けると、裏でCloud Buildが自動でコンテナを焼いて、Artifact Registryに保存し、そのままCloud Runに載せてくれる。手元にDockerが入っていなくても動きます。
PROJECT_ID="my-project-123"
REGION="asia-northeast1"
SERVICE="myapp-api"
gcloud config set project "$PROJECT_ID"
# 必要なAPIをまとめて有効化(初回だけ)
gcloud services enable \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com \
secretmanager.googleapis.com
# カレントディレクトリのソースから、ビルドしてそのままデプロイ
gcloud run deploy "$SERVICE" \
--source . \
--region "$REGION" \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 20 \
--set-env-vars NODE_ENV=production
コマンドが終わると、https://myapp-api-xxxx-an.a.run.app のようなURLが表示されます。そこに/healthを付けて叩けば、もう本番です。初回はビルドで2〜3分かかりますが、2回目以降はキャッシュが効いて速くなります。
--allow-unauthenticatedは「誰でもアクセス可」の意味です。社内専用APIなら外してください。外すと未認証アクセスが弾かれ、Cloud Run Invoker権限を持つ呼び出し元だけが叩けるようになります。Webhookのように外部から叩かれる口は付ける、内部APIは付けない、と用途で分けます。
Dockerfileを自分で握る(Claude Codeにレビューさせる)
--source .は手軽ですが、本番ではDockerfileを自分で持っておく方が安心です。イメージサイズ、実行ユーザー、起動コマンドを自分でコントロールできるからです。とはいえ、いいDockerfileを毎回ゼロから書くのは面倒。僕はClaude Codeに条件を渡して叩き台を出させ、それを直して使っています。
claude -p "
このCloud Run向けDocker構成をレビューして改善して。
要件:
- Node.js 22 LTS / TypeScript / Express
- ランタイムイメージには本番依存だけ入れる(マルチステージ)
- 非rootユーザーで動かす
- 環境変数PORTを待ち受ける
- .dockerignoreも出す
- Cloud Run特有のセキュリティ・コスト上のリスクを指摘して
最終的なDockerfileと、短いレビューチェックリストを返して。
"
出てくるのは、こういうマルチステージ構成です。ビルド用と実行用を分けて、実行イメージを軽くするのが定石です。
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# rootではなく専用ユーザーで動かす
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER appuser
# PORTはCloud Runが渡すので EXPOSE は目安。CMDでPORTを固定しないこと
CMD ["node", "dist/index.js"]
.dockerignoreで不要物を除くと、ビルドとコールドスタートが軽くなります。
node_modules
dist
.env
.env.*
*.log
.git
Dockerfile
README.md
このDockerfileを使う場合は、--source .の代わりに自分でビルド・pushしたイメージを指定します。
REPOSITORY="myapp"
IMAGE="$REGION-docker.pkg.dev/$PROJECT_ID/$REPOSITORY/api:v1.0.0"
# イメージ倉庫を作る(初回だけ)
gcloud artifacts repositories create "$REPOSITORY" \
--repository-format=docker \
--location="$REGION"
# DockerをArtifact Registryに認証させる
gcloud auth configure-docker "$REGION-docker.pkg.dev"
docker build -t "$IMAGE" .
docker push "$IMAGE"
gcloud run deploy "$SERVICE" \
--image "$IMAGE" \
--region "$REGION" \
--allow-unauthenticated
Dockerfileの細かい詰め方はClaude CodeとDockerの実戦メモに分けて書いています。Composeやキャッシュ戦略まで踏み込みたいときはそちらへ。
0までスケールする仕組みと、その代償
Cloud Run最大の魅力が、この0スケールです。--min-instances 0なら、リクエストが来ない間はコンテナが1台も立たない。リクエスト課金モードなら、その待機時間は課金されません。個人開発のWebhookや検証環境には、これがどんぴしゃでハマります。
ただし代償があります。コールドスタートです。0台から立ち上げる最初の1リクエストは、コンテナ起動の分だけ待たされる。軽いNodeアプリでも数百ミリ秒〜数秒、依存が重いと体感できる遅さになります。
対策は環境ごとに分けるのが現実的です。
- 個人開発・検証・夜間アクセスがほぼ無い環境 →
--min-instances 0。とにかく安く。 - 業務API・初回遅延を嫌うWebhook →
--min-instances 1以上で常に1台を温めておく。
# 本番:最低1台を常駐させてコールドスタートを潰す
gcloud run services update "$SERVICE" \
--region "$REGION" \
--min-instances 1 \
--max-instances 20 \
--concurrency 40
ここで--concurrencyも一緒に決めます。これは「1台のコンテナが同時に捌くリクエスト数」です。gcloudでデプロイした場合のデフォルトは80(vCPU 1あたり)、最大1000まで上げられます。ただし上げすぎ注意。Node自体は同時接続に強くても、DBコネクションプールや外部SaaSのレート制限が先に詰まります。僕はWebhookで一度concurrencyを上げすぎて、DB接続が枯れてエラーを量産しました。署名検証とDB書き込みが絡む口は20〜40から、軽いステータスAPIは80以上から、と中身で決めます。
料金の考え方(無料枠と3つのつまみ)
Cloud Runの請求は、つまるところ3つの軸です。
- vCPU秒 — CPUを使った時間
- メモリGB秒 — 確保したメモリ × 時間
- リクエスト数 — 受けたHTTPリクエストの数
そして毎月の無料枠が用意されています。執筆時点では200万リクエスト / 36万GB秒のメモリ / 18万vCPU秒が常に無料(リクエスト課金時)。個人開発レベルなら、ここに収まって実質0円ということも珍しくありません。
コストを左右する設定は、デプロイ時のこの3つです。
| つまみ | 効果 | 目安 |
|---|---|---|
--memory | 確保メモリ。多いほどGB秒が増える | まず512Mi、足りなければ上げる |
--cpu | vCPU数。多いほどvCPU秒が増える | 1で始める |
--min-instances | 常駐台数。0なら待機中ゼロ円 | 検証0/本番1以上 |
最大の落とし穴は、検証環境に--min-instances 1を入れっぱなしにすることです。誰も叩いていなくても1台が常駐し、その分の課金が静かに積み上がります。「あれ、何もしてないのに毎月数百円取られてる」の原因はだいたいこれ。検証が終わったら0に戻す癖をつけてください。正確な単価はリージョンで変わるので、Cloud Run公式ドキュメントの料金ページで最新を確認するのが確実です。
なお、リクエストの最大実行時間(タイムアウト)はデフォルト5分、最長60分まで延ばせます。重い処理を載せるなら、この上限も頭に入れておきます。
ログ確認と、壊れたときの戻し方
公開したら終わり、ではありません。Cloud Runのログ(リクエスト・コンテナ・システム)はCloud Loggingに自動で集まります。アプリは標準出力にJSONっぽく吐いておけば、まずは十分追えます。
gcloud run services logs read "$SERVICE" \
--region "$REGION" \
--limit 20
そして一番大事なのがロールバックです。Cloud Runはデプロイや設定変更のたびに「リビジョン」という不変の版を作ります。新しいデプロイで不具合が出たら、トラフィックを前のリビジョンに丸ごと戻すだけ。再ビルドは要りません。
# リビジョン一覧を見る
gcloud run revisions list \
--service "$SERVICE" \
--region "$REGION"
# 特定リビジョンに100%トラフィックを戻す
gcloud run services update-traffic "$SERVICE" \
--region "$REGION" \
--to-revisions myapp-api-00012-abc=100
このlistとupdate-trafficは、障害が起きる前に平時で一度叩いておくこと。本番が燃えてからコマンドを調べ始めるのは最悪です。僕はこれを「消火訓練」と呼んで、デプロイ手順に必ず入れています。
秘密情報の扱いも一言。DB接続文字列やJWT署名鍵を--set-env-varsに平文で書くのはやめてください。設定履歴やCIログに残ります。Secret Managerに入れて--set-secrets DATABASE_URL=DATABASE_URL:latestの形で渡すのが安全です。鍵の管理全般はシークレット管理ガイドにまとめています。
僕がCloud Runでやらかした失敗3つ
正直に書きます。最初はミスの連続でした。
ひとつ目は、冒頭のPORT固定。app.listen(8080)とベタ書きして、コンテナは起動するのにヘルスチェックが通らない。ログには何も出ず、ただ「起動失敗」とだけ。process.env.PORTに直した瞬間、嘘のように動きました。
ふたつ目は、Lambdaの気分で巨大な依存を全部詰めたこと。Cloud Runはコンテナだから何でも入りますが、イメージが大きいほどビルド・push・コールドスタートが全部重くなる。マルチステージと.dockerignoreで実行イメージを削ったら、コールドスタートが目に見えて軽くなりました。
みっつ目は、Secret Managerを使ってる「つもり」だったこと。実際は--set-env-varsに秘密値が混ざっていて、Cloud Buildのログに鍵が残っていました。気づいてローテーションする羽目に。最初から--set-secretsで統一しておけばよかった話です。
よくある質問
Q. Cloud RunとCloud Functionsはどう使い分ける? A. 「Dockerfileを書けるWebアプリ/APIまるごと」ならCloud Run、「数行で終わる単機能のイベント処理」ならCloud Functions。実体は近づいていますが、コンテナを丸ごと持ち込みたいかどうかで選ぶと迷いません。
Q. --source .と自前Dockerfile、どっちを使うべき?
A. 検証や最初の一歩は--source .が圧倒的にラク。本番運用に入ったら、イメージの中身を握れる自前Dockerfileへ移行するのがおすすめです。両方とも裏ではコンテナを作っているので、考え方は同じです。
Q. コールドスタートはどれくらい遅い?
A. 軽いNodeアプリで数百ミリ秒〜数秒、依存が重いほど伸びます。気になるなら--min-instances 1で常時1台を温めるか、イメージを軽くして起動を速くします。
Q. 0スケールにすると本当に無料?
A. リクエスト課金モードなら、待機中(0台のとき)は課金されません。加えて毎月の無料枠(200万リクエスト等)もあるので、低トラフィックなら実質0円になりがちです。ただし常駐させる--min-instances 1は待機中も課金対象です。
Q. デプロイで本番が壊れたらどうする?
A. gcloud run services update-trafficで前のリビジョンに100%戻すだけ。リビジョンは不変なので再ビルド不要で即復旧できます。事前に一度練習しておきましょう。
実際に試した結果
この記事のサンプルは、ローカルでnpm run devして/healthが返ることを確認し、gcloud run deploy --source .で実際にデプロイしてURL経由の疎通まで通しています。一番効いたのは、やはりPORTを環境変数から読むことと、--min-instances 0で検証環境を「叩かなければ無料」に保つこと。この2点だけで、置き場所のストレスと請求の不安がほぼ消えました。
最後にClaude Codeの使いどころを。Cloud Runは「作って」より「デプロイ前に壊れる点をレビューして」と頼むと化けます。PORT待受、SIGTERMのgraceful shutdown、非rootコンテナ、Secret Managerの使い方、min/max-instancesとコスト、ロールバック手順——このあたりを一括でチェックさせると、本番事故の芽を先に潰せます。チームで運用ルールごと整えたいなら、Claude Code研修・導入相談も用意しています。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
制作会社がClaude Codeに触らせる前に決める権限チェックリスト
クライアントサイトを壊さずにAI編集を使うための、制作会社向け権限と確認の型です。
SaaSサポートのバグ報告をClaude Codeで再現手順に変える実務フロー
問い合わせ文をそのまま開発へ投げず、再現手順、証拠、次の一手に整えるサポート向け手順です。
Obsidianの古いメモをClaude Codeの指示書に変える10分ルーチン
Obsidianに溜めたメモが毎回ゴミになる人へ。事実・決定・未確認に仕分けして、Claude Codeがそのまま動ける指示書に変える朝の10分の型を紹介します。