1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HonoアプリをDockerコンテナ化してCloud Runにデプロイする

1
Last updated at Posted at 2026-09-20

この記事について

  • Honoで簡単なJSON API(Todo風CRUD)を作る
  • Dockerでコンテナ化してローカル動作確認
  • Google Cloud Runにデプロイして公開する
  • Vitestでテストを書く
  • GitHub Actionsでテスト自動化 + mainマージでCloud Runへ自動デプロイ

前回はCloudflare Workersでリンクページを作ったので、今回はGCPのCloud Runでコンテナベースのデプロイを体験してみます。写経しながら手を動かして理解する形式です。

全体構成

スクリーンショット 2026-09-21 011515.png

[開発]
  Hono(Node.js) + TypeScript
        │
        │ docker build
        ▼
  Dockerイメージ
        │
        │ push
        ▼
  Artifact Registry(GCP)
        │
        │ deploy
        ▼
  Cloud Run(本番)

[CI/CD]
git push
  ├─ GitHub Actions: npm test
  └─ main反映後: docker build → Artifact Registryへpush → Cloud Runへdeploy

事前準備

以下を用意します。

  1. Node.js 20以上(node -vで確認)
  2. Docker Desktop(インストール済みでない場合は公式サイトから)
  3. Google Cloudアカウント(無料枠あり。初回は$300分のクレジットが付与される)
  4. gcloud CLI(公式手順からインストール)

gcloud CLIをインストールしたら、ログインしておきます。

gcloud auth login

GCPプロジェクトの作成

gcloud projects create my-hono-app --name="my-hono-app"
gcloud config set project my-hono-app

my-hono-appの部分はGCP全体でユニークな名前にする必要があるので、重複したら別名にしてください(例: my-hono-app-20260920)。

課金アカウントの紐付けが必要です。GCPコンソールの「お支払い」からプロジェクトに課金アカウントを紐付けてください(無料枠内なら課金は発生しません)。

必要なAPIを有効化

gcloud services enable run.googleapis.com
gcloud services enable artifactregistry.googleapis.com
gcloud services enable cloudbuild.googleapis.com

1. Honoアプリの作成

1-1. プロジェクト作成

npm create hono@latest todo-api

テンプレートを聞かれたら nodejs を選んでください。Cloud Runは通常のNode.jsサーバーとしてコンテナを起動するので、Workers用テンプレートではなくNode.js用テンプレートを使います。

cd todo-api
npm install

1-2. 型とデータストアを作成

src/todos.ts を新規作成します。

export type Todo = {
  id: number
  title: string
  done: boolean
}

let todos: Todo[] = [
  { id: 1, title: "Honoを学ぶ", done: false },
  { id: 2, title: "Cloud Runにデプロイする", done: false },
]

let nextId = 3

export function getAll(): Todo[] {
  return todos
}

export function getById(id: number): Todo | undefined {
  return todos.find((t) => t.id === id)
}

export function create(title: string): Todo {
  const todo: Todo = { id: nextId++, title, done: false }
  todos.push(todo)
  return todo
}

export function remove(id: number): boolean {
  const before = todos.length
  todos = todos.filter((t) => t.id !== id)
  return todos.length < before
}

インメモリの簡易ストアです(コンテナが再起動すると消えます。今回はデプロイ手順の学習が目的なのでこれでOKです)。

1-3. APIエンドポイントを作成

src/index.ts を以下の内容に書き換えます。

import { serve } from "@hono/node-server"
import { Hono } from "hono"
import { getAll, getById, create, remove } from "./todos.js"

const app = new Hono()

app.get("/", (c) => {
  return c.json({ message: "Todo API is running" })
})

app.get("/api/todos", (c) => {
  return c.json(getAll())
})

app.get("/api/todos/:id", (c) => {
  const id = Number(c.req.param("id"))
  const todo = getById(id)
  if (!todo) {
    return c.json({ error: "not found" }, 404)
  }
  return c.json(todo)
})

app.post("/api/todos", async (c) => {
  const body = await c.req.json<{ title?: string }>()
  if (!body.title) {
    return c.json({ error: "title is required" }, 400)
  }
  const todo = create(body.title)
  return c.json(todo, 201)
})

app.delete("/api/todos/:id", (c) => {
  const id = Number(c.req.param("id"))
  const ok = remove(id)
  if (!ok) {
    return c.json({ error: "not found" }, 404)
  }
  return c.json({ ok: true })
})

const port = Number(process.env.PORT) || 8080

serve({
  fetch: app.fetch,
  port,
})

console.log(`Server is running on port ${port}`)

ポイントは process.env.PORT を見ていることです。Cloud RunはコンテナにPORT環境変数を渡してリクエストを送ってくるので、これを見てlistenする必要があります(ローカルでは未設定なら8080番を使います)。

1-4. 動作確認

npm run dev

別のターミナルで確認します。

curl http://localhost:8080/api/todos
curl -X POST http://localhost:8080/api/todos -H "Content-Type: application/json" -d '{"title":"写経する"}'
curl http://localhost:8080/api/todos/1
curl -X DELETE http://localhost:8080/api/todos/1

それぞれ期待通りのJSONが返ってくればOKです。

2. テストを書く

2-1. Vitestのインストール

npm install -D vitest

package.jsonのscriptsに追加します。

{
  "scripts": {
    "test": "vitest run"
  }
}

2-2. テストコード

src/todos.test.ts を新規作成します。

import { describe, it, expect } from "vitest"
import { getAll, getById, create, remove } from "./todos.js"

describe("todos", () => {
  it("getAll returns an array", () => {
    expect(Array.isArray(getAll())).toBe(true)
  })

  it("create adds a new todo", () => {
    const before = getAll().length
    const todo = create("テスト用タスク")
    expect(getAll().length).toBe(before + 1)
    expect(todo.title).toBe("テスト用タスク")
    expect(todo.done).toBe(false)
  })

  it("getById finds the created todo", () => {
    const todo = create("検索テスト")
    const found = getById(todo.id)
    expect(found?.title).toBe("検索テスト")
  })

  it("remove deletes a todo", () => {
    const todo = create("削除される予定")
    const ok = remove(todo.id)
    expect(ok).toBe(true)
    expect(getById(todo.id)).toBeUndefined()
  })

  it("remove returns false for non-existent id", () => {
    const ok = remove(999999)
    expect(ok).toBe(false)
  })
})

実行して確認します。

npm test

全件パスすればOKです。

3. Dockerでコンテナ化

スクリーンショット 2026-09-21 011525.png

3-1. Dockerfile作成

プロジェクト直下にDockerfileを作成します。

# ビルド用ステージ
FROM node:20-slim AS builder

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# 実行用ステージ
FROM node:20-slim

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY --from=builder /app/dist ./dist

ENV NODE_ENV=production
EXPOSE 8080

CMD ["node", "dist/index.js"]

マルチステージビルドにすることで、本番イメージには開発用の依存関係やソースコードを含めず、ビルド成果物だけを含めた軽量なイメージになります。

package.jsonのscriptsにbuildがなければ追加してください(テンプレートによっては既にある場合があります)。

{
  "scripts": {
    "build": "tsc"
  }
}

tsconfig.jsonのoutDirがdistになっているか確認してください。

3-2. .dockerignore作成

.dockerignoreを作成し、不要なファイルをビルドコンテキストから除外します。

node_modules
dist
.git
*.md
.env

3-3. ローカルでビルド・起動確認

docker build -t todo-api .
docker run -p 8080:8080 todo-api

別ターミナルから確認します。

curl http://localhost:8080/api/todos

ローカルのnpm run devと同じ結果が返ってくればOKです。Ctrl+Cでコンテナを止めておきます。

4. Cloud Runへ手動デプロイ

4-1. Artifact Registryにリポジトリを作成

Dockerイメージの置き場所を作ります。

gcloud artifacts repositories create todo-api-repo \
  --repository-format=docker \
  --location=asia-northeast1 \
  --description="todo-api docker images"

4-2. Dockerの認証設定

gcloud auth configure-docker asia-northeast1-docker.pkg.dev

4-3. イメージのビルド・タグ付け・push

macOS/Linux(bash/zsh)の場合:

export PROJECT_ID=$(gcloud config get-value project)

docker build -t asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-api-repo/todo-api:latest .

docker push asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-api-repo/todo-api:latest

Windows(PowerShell)の場合:

$env:PROJECT_ID = (gcloud config get-value project)

docker build -t asia-northeast1-docker.pkg.dev/$env:PROJECT_ID/todo-api-repo/todo-api:latest .

docker push asia-northeast1-docker.pkg.dev/$env:PROJECT_ID/todo-api-repo/todo-api:latest

PowerShellではexportは使えず、$PROJECT_IDのような書き方も反映されません。環境変数は$env:変数名でセット・参照します。また環境変数はそのターミナルのセッション内でしか有効でないため、別のターミナルタブやウィンドウを開くと消える点にも注意してください。以降のコマンドでも$PROJECT_IDはPowerShellでは$env:PROJECT_IDと読み替えてください。

4-4. Cloud Runへデプロイ

gcloud run deploy todo-api \
  --image=asia-northeast1-docker.pkg.dev/$PROJECT_ID/todo-api-repo/todo-api:latest \
  --region=asia-northeast1 \
  --platform=managed \
  --allow-unauthenticated

--allow-unauthenticatedは未認証アクセスを許可するオプションです(誰でもアクセスできる公開APIにする場合はこれが必要)。

デプロイが完了すると、以下のようなURLが表示されます。

Service URL: https://todo-api-xxxxxxxxxx-an.a.run.app

このURLにアクセスして確認します。

curl https://todo-api-xxxxxxxxxx-an.a.run.app/api/todos

ローカルと同じ結果が返ってくれば手動デプロイは成功です。

5. GitHub Actionsでテスト自動化

5-1. GitHubリポジトリの作成とプッシュ

git init
git add .
git commit -m "Initial commit: todo-api"
git branch -M main
git remote add origin https://github.com/<あなたのユーザー名>/todo-api.git
git push -u origin main

5-2. テスト用ワークフロー

.github/workflows/ci.ymlを作成します。

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - run: npm ci
      - run: npm test
  • push/pull_requestのどちらでもmainブランチ向けなら実行されます
  • npm ciはロックファイル通りに厳密インストールする、CI向けのコマンドです
  • テストが落ちればここで検知でき、以降のデプロイに進みません

コミットしてpushし、GitHubのActionsタブで緑のチェックが付くことを確認してください。

git add .github/workflows/ci.yml
git commit -m "Add CI workflow"
git push

6. GitHub ActionsでCloud Runへ自動デプロイ

6-1. サービスアカウントの作成

デプロイ用の専用サービスアカウントを作ります。

gcloud iam service-accounts create github-deployer \
  --display-name="GitHub Actions Deployer"

必要な権限を付与します。名前は必ず直前のcreateコマンドで作成したサービスアカウント名と一致させてください(github-deployerの部分を書き換えていたら、以降も同じ名前で揃えます)。

macOS/Linux(bash/zsh)の場合:

export SA_EMAIL=github-deployer@$PROJECT_ID.iam.gserviceaccount.com

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/run.admin"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/artifactregistry.writer"

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:$SA_EMAIL" \
  --role="roles/iam.serviceAccountUser"

Windows(PowerShell)の場合:

$env:SA_EMAIL = "github-deployer@$env:PROJECT_ID.iam.gserviceaccount.com"

gcloud projects add-iam-policy-binding $env:PROJECT_ID `
  --member="serviceAccount:$env:SA_EMAIL" `
  --role="roles/run.admin"

gcloud projects add-iam-policy-binding $env:PROJECT_ID `
  --member="serviceAccount:$env:SA_EMAIL" `
  --role="roles/artifactregistry.writer"

gcloud projects add-iam-policy-binding $env:PROJECT_ID `
  --member="serviceAccount:$env:SA_EMAIL" `
  --role="roles/iam.serviceAccountUser"

PowerShellで文字列を組み立てるときは""で囲む必要があります。囲まずに$env:SA_EMAIL = github-deployer@...と書くと、コマンドとして実行しようとしてエラーになります。

6-2. 認証キーの発行

macOS/Linux(bash/zsh)の場合:

gcloud iam service-accounts keys create key.json \
  --iam-account=$SA_EMAIL

Windows(PowerShell)の場合:

gcloud iam service-accounts keys create key.json `
  --iam-account=$env:SA_EMAIL

key.jsonが手元に生成されます。このファイルは絶対にGitにコミットしないでください。

サービスアカウントキーは漏洩リスクがあるため、本来はWorkload Identity連携(キーレス認証)が推奨されます。今回は入門向けにシンプルなキー方式で進めますが、本番運用するなら移行を検討してください。

6-3. GitHub Secretsへ登録

GitHubリポジトリの Settings > Secrets and variables > Actions から、以下のSecretを登録します。

Secret名 値
GCP_PROJECT_ID $PROJECT_IDの値
GCP_SA_KEY key.jsonの中身をそのままコピー&ペースト

登録し終えたら、ローカルのkey.jsonは削除しておきましょう。

rm key.json

6-4. デプロイ用ワークフロー

.github/workflows/deploy.ymlを作成します。

name: Deploy to Cloud Run

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: google-github-actions/auth@v2
        with:
          credentials_json: "${{ secrets.GCP_SA_KEY }}"

      - uses: google-github-actions/setup-gcloud@v2

      - name: Configure Docker
        run: gcloud auth configure-docker asia-northeast1-docker.pkg.dev

      - name: Build and push image
        run: |
          docker build -t asia-northeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/todo-api-repo/todo-api:${{ github.sha }} .
          docker push asia-northeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/todo-api-repo/todo-api:${{ github.sha }}

      - name: Deploy to Cloud Run
        run: |
          gcloud run deploy todo-api \
            --image=asia-northeast1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/todo-api-repo/todo-api:${{ github.sha }} \
            --region=asia-northeast1 \
            --platform=managed \
            --allow-unauthenticated \
            --project=${{ secrets.GCP_PROJECT_ID }}

ポイント:

  • testジョブが失敗するとdeployジョブは実行されません(needs: test)
  • イメージタグに${{ github.sha }}(コミットハッシュ)を使うことで、どのコミットのイメージがデプロイされたか追跡できます
  • google-github-actions/authでサービスアカウントキーを使って認証し、setup-gcloudでgcloud CLIをセットアップします

コミットしてpushします。

git add .github/workflows/deploy.yml
git commit -m "Add Cloud Run deploy workflow"
git push

GitHubのActionsタブでtest→deployの順に緑になり、Cloud RunのURLにアクセスして最新の変更が反映されていれば成功です。

7. CI/CDの役割分担

スクリーンショット 2026-09-21 011535.png

git push (main)
  │
  ├─ test ジョブ
  │    ├─ npm ci
  │    └─ npm test ← ここで落ちればdeployは実行されない
  │
  └─ deploy ジョブ(testの後)
       ├─ docker build
       ├─ Artifact Registryへpush
       └─ Cloud Runへdeploy

前回のCloudflare Workers版では「GitHub Actionsでテスト」「Cloudflareのビルドコマンドで最終ゲート」という二段構えでしたが、今回はGitHub Actions一本でtest→deployまで完結させる構成です。Cloud RunにはCloudflare Pagesのようなビルド時テスト機構がないため、CI側でゲートを持たせる必要があります。

まとめ

  • Hono(Node.jsテンプレート) + @hono/node-serverでCloud Run向けのAPIサーバーを作成
  • マルチステージDockerfileでビルド成果物のみを含む軽量イメージを作成
  • Artifact Registryにイメージをpushし、Cloud Runへデプロイ
  • Vitestでロジックのテストを書き、GitHub Actionsでtest→build→deployを自動化
  • サービスアカウントキーでの認証は入門向けの簡易構成。本番ではWorkload Identity連携への移行を検討する

前回のCloudflare Workers版と比べると、Cloud Runは「コンテナで動く」という自由度の高さがある一方、Dockerfileの管理やイメージのビルド・pushといった一手間が増える構成になっています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?