言語を切り替える
テーマを切り替える

GitHub Actions デプロイ戦略:VPSからクラウドプラットフォームへのCDパイプライン

はじめに

深夜3時、私は GitHub Actions のログ画面を凝視していました。赤いエラーが一行ずつ上へ流れていきます。「Host key verification failed」。またしても SSH の問題です。

これでデプロイ失敗は5回目。ローカルテストは全部通っているのに、GitHub にプッシュすると爆発する。その瞬間、思わず悪態をつきたくなりました。でも同時に気づいたのです。デプロイ戦略の選択は、思っていたよりずっと複雑だと。

自分で管理する VPS でも、Vercel や Cloudflare Pages のようなホスティングプラットフォームでも、どの方法にも落とし穴があります。選択を誤れば、深夜のデバッグ回数は増える一方です。

この記事では、GitHub Actions のいくつかのデプロイ戦略について語り、あなたに合った道を見つける手助けをします。


VPS SSH デプロイ:オールドスクールだが信頼できる

正直に言うと、最初は VPS デプロイに強い抵抗がありました。面倒くさいと思ったのです。SSH 鍵、known_hosts、rsync のパラメータ……やることが山ほどある、と。

でも何度か痛い目に遭ううちに、この「オールドスクール」なやり方こそ最も制御しやすいと気づきました。

SSH 鍵の設定:ハードコードしない

最もよくある悩みは、SSH 鍵をどこに置くかです。

初心者はよく秘密鍵を直接 workflow ファイルに書き込みます。これは大きな間違い。GitHub Secrets こそ正しい置き場所です。

リポジトリの Settings → Secrets → Actions で SSH_PRIVATE_KEY を追加します。そして workflow ではこう使います:

- name: Setup SSH
  uses: webfactory/ssh-agent-action@v0.7.0
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

この action は自動で ssh-agent を起動し、鍵を読み込んでくれます。手間いらずです。

known_hosts:「Host key verification failed」を回避する

SSH が初めてサーバーへ接続するとき、この host を信頼するか尋ねてきます。対話式の問答は CI 環境では処理できないため、あらかじめサーバーのフィンガープリントを known_hosts に追加しておく必要があります。

方法は2つあります:

方法1:action で自動追加する

- name: Add server to known hosts
  uses: webfactory/ssh-agent-action@v0.7.0
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
    known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}

SSH_KNOWN_HOSTS の内容はこのように取得できます:

ssh-keyscan -H your-server.com >> known_hosts.txt
# ファイルの内容を GitHub Secrets にコピーする

方法2:手動で設定する

- name: Add server to known hosts
  run: |
    mkdir -p ~/.ssh
    ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

方法1のほうがクリーン、方法2は素早いデバッグ向きです。

rsync か scp か?

デプロイ時のファイル転送には、私は rsync を使います。理由はシンプルです:

  • 変更のあったファイルだけを転送し、時間を節約できる
  • 特定ディレクトリ(node_modules など)を除外できる
  • 増分同期に対応している

典型的な rsync コマンドはこうです:

- name: Deploy to server
  run: |
    rsync -avz --delete \
      --exclude 'node_modules' \
      --exclude '.git' \
      ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/html/

--delete パラメータは、転送先にあって転送元に無いファイルを削除します。使うときは慎重に。設定を間違えると、消してはいけないものまで消えてしまいます。

デプロイ後のコマンド:サービスを再起動する

静的サイトなら転送が終われば完了です。でも Node.js アプリをデプロイする場合は、サービスの再起動も必要です。

私は Node プロセスの管理に PM2 を使うのが好きです。デプロイ後にこう実行します:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/app && pm2 restart all"

あるいは、より安全に特定のアプリだけ再起動する方法もあります:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "pm2 restart my-app --update-env"

--update-env は環境変数を再読み込みするので、設定に変更があった場合に向いています。


クラウドプラットフォームデプロイ:ホスティングサービスの手軽さ

VPS デプロイの問題は、サーバーを自分で管理しなければならないことです。セキュリティパッチ、SSL 証明書の更新、ファイアウォールのルール……雑務が山積みです。

ホスティングプラットフォームならずっと楽です。コードをプッシュすれば、自動でビルドして自動でデプロイ。あなたはコードを書くことだけに集中できます。

Vercel:フロントエンドプロジェクトの第一選択

Vercel のフロントエンドプロジェクトへの対応は、ほぼ完璧です。Next.js、Astro、React——ワンクリックでデプロイ、設定ゼロ。

ただしプロジェクトにバックエンド API が必要な場合は注意が必要です。Vercel の Serverless Functions には実行時間の制限があります(無料版10秒、Pro 版60秒)。超過すると timeout します。

純粋な静的サイトやシンプルな API なら、Vercel で十分です。複雑なバックエンドサービスはやはり自前で用意するしかありません。

GitHub Actions で Vercel にデプロイする設定はこうです:

name: Deploy to Vercel

on:
  push:
    branches: [main]

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

      - name: Install Vercel CLI
        run: npm i -g vercel@latest

      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

VERCEL_TOKEN は Vercel のコンソールで生成し、GitHub Secrets に保存します。

Cloudflare Pages:無料枠が寛大

Cloudflare Pages の無料枠は Vercel よりずっと寛大です。帯域は無制限、ビルド回数は月500回——個人プロジェクトには余裕で足ります。

しかも Cloudflare のグローバル CDN は本当に速い。私自身で計測しましたが、アジアからのアクセス速度は Vercel より安定していました。

デプロイ設定はこうです:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

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

      - name: Build
        run: npm run build

      - name: Deploy
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-project
          directory: dist

Cloudflare にはもう一つ利点があります。R2 ストレージの無料枠も大きいのです。静的アセットを R2 に置き、Pages の CDN と組み合わせれば、読み込み速度をかなり上げられます。

Netlify:老舗の安定派

Netlify は前の2つほど使っていませんが、老舗のホスティングプラットフォームでエコシステムも成熟しています。

デプロイ設定も似たようなものです:

- name: Deploy to Netlify
  uses: netlify/actions/cli@master
  with:
    args: deploy --prod
  env:
    NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
    NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Netlify の Form handling 機能はなかなか実用的です。フォーム送信を自動で処理してくれるので、シンプルなマーケティングページに向いています。

ホスティングプラットフォームの制約

とはいえ、ホスティングプラットフォームも万能ではありません。

よくある制約をいくつか挙げます:

  1. ビルド環境の制限:メモリも CPU も上限があり、大規模プロジェクトはビルドに失敗することがある
  2. カスタマイズ性の低さ:nginx 設定を変えたい?できません
  3. プラットフォーム依存:プラットフォームが倒産したり方針を変えたりすれば、移行を迫られる
  4. 国内アクセスの問題:一部のプラットフォームは国内からのアクセスが不安定(Cloudflare は改善されているが)

プロジェクトに完全な制御が必要なら、やはり VPS に戻るしかありません。


ハイブリッド戦略:柔軟性と制御を両立する

多くのプロジェクトは「純粋な静的」でも「純粋なバックエンド」でもありません。フロントエンドは Next.js、バックエンドはデータベースに接続して定期タスクも走らせる……。

そんなときは、ハイブリッドデプロイが最適解になり得ます。

静的ページはホスティング + API は VPS にデプロイ

典型的な構成はこうです:

  • 静的ページ(HTML/CSS/JS)は Cloudflare Pages か Vercel にデプロイ
  • Node.js の API サービスは自分の VPS にデプロイ
  • データベースも VPS 上に置く(または Supabase/PlanetScale でホスティング)

メリットはそれぞれの強みを活かせること:

  • フロントエンドは CDN 高速化と自動 HTTPS の恩恵を受けられる
  • バックエンドは完全な制御権を持ち、ホスティングプラットフォームの制約を受けない
  • データベースアクセスの遅延が低い(API とデータベースが同じマシン上にある)

GitHub Actions のマルチステージデプロイ

1つの workflow で両方の場所へ同時にデプロイします:

name: Hybrid Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-path: ./dist
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist

  deploy-frontend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-frontend
          directory: dist

  deploy-backend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup SSH
        uses: webfactory/ssh-agent-action@v0.7.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Deploy API to VPS
        run: |
          rsync -avz --delete \
            --exclude 'node_modules' \
            ./api/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/api/
      - name: Restart API service
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
            "cd /var/www/api && npm install && pm2 restart api"

この workflow には3つの job があります:

  1. build:プロジェクトをビルドし、静的ファイルを生成する
  2. deploy-frontend:静的ファイルを Cloudflare Pages にデプロイする
  3. deploy-backend:API を VPS にデプロイしてサービスを再起動する

needs: build によって、デプロイ job はビルド完了後にのみ実行されます。upload-artifactdownload-artifact が job 間でビルド成果物を受け渡します。

環境変数の分離

ハイブリッドデプロイの課題の一つは、フロントエンドとバックエンドで環境変数が異なることです。

フロントエンドは API のアドレスを知る必要があり、バックエンドはデータベースのパスワードを知る必要があります。

私のやり方はこうです:

# フロントエンド job
- name: Set frontend env
  run: |
    echo "API_URL=https://api.mydomain.com" >> $GITHUB_ENV

# バックエンド job
- name: Deploy with env
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/api && pm2 restart api --update-env DATABASE_URL=${{ secrets.DATABASE_URL }}"

機密情報(データベースのパスワード、API トークン)は必ず GitHub Secrets を通します。非機密情報(API のアドレス)は workflow に書いて構いません。


実戦設定の例

以下は、これまでに触れたポイントをすべて網羅した、完全な VPS デプロイ workflow です。

完全な workflow ファイル

name: Deploy to VPS

on:
  push:
    branches: [main]
  workflow_dispatch:  # 手動でデプロイを発火

env:
  NODE_VERSION: '20'

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist
          retention-days: 1

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Setup SSH
        uses: webfactory/ssh-agent-action@v0.7.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy files
        run: |
          rsync -avz --delete \
            --exclude '.htaccess' \
            ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}

      - name: Verify deployment
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} \
            "ls -la ${{ secrets.DEPLOY_PATH }}"

      - name: Send deployment notification
        if: always()
        run: |
          curl -X POST "${{ secrets.NOTIFICATION_WEBHOOK }}" \
            -H "Content-Type: application/json" \
            -d '{"text": "Deployment completed: ${GITHUB_SHA}"}'

設定が必要な Secrets

Secret 名説明取得方法
SSH_PRIVATE_KEYSSH 秘密鍵の内容ローカルで生成、公開鍵はサーバーに置く
SERVER_HOSTサーバーの IP またはドメインあなたの VPS の情報
SERVER_USERSSH ログインのユーザー名通常は rootubuntu
DEPLOY_PATHデプロイ先のパス例:/var/www/html
NOTIFICATION_WEBHOOKデプロイ通知の送信先Slack/Telegram の webhook

頻出トラブルの調査

デプロイに失敗したとき、ログを見ても情報が多すぎて目が回りがちです。

私の調査順序はこうです:

  1. SSH 接続の問題:「Setup SSH」と「Add server to known hosts」のステップを見る
    • 失敗していたら、鍵の形式と known_hosts の内容を確認
  2. rsync 転送の問題:「Deploy files」のステップを見る
    • 失敗していたら、パスの存在と権限の正しさを確認
  3. サービス再起動の問題:「Verify deployment」のステップを見る
    • 失敗していたら、デプロイ先にファイルがあるかを確認

ちょっとしたコツ:失敗したステップの後ろにデバッグ出力を追加します。

- name: Debug SSH connection
  if: failure()
  run: |
    echo "SSH config:"
    cat ~/.ssh/config || echo "No config file"
    echo "Known hosts:"
    cat ~/.ssh/known_hosts || echo "No known_hosts file"
    ssh -v ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} echo "Connection test"

ssh -v は詳細なログを出力するので、問題がどこにあるか見えてきます。


まとめ

これだけ書いてきましたが、要するに一言です。完璧なデプロイ方法は存在せず、あるのはあなたのプロジェクトに最も合った方法だけ。

選定のアドバイス

  • 純粋な静的サイト(ブログ、ドキュメント):Cloudflare Pages か Vercel で手間いらず
  • シンプルな API + フロントエンド:ホスティングプラットフォームで十分、VPS で苦労しない
  • 複雑なバックエンド + データベース:VPS かクラウドサーバー、制御権が重要
  • ハイブリッド構成:フロントエンドはホスティング + バックエンドは VPS、それぞれの強みを活かす

どれを選んでも、GitHub Actions の設定パターンはほぼ同じです。ビルド → 転送 → 再起動。この3ステップを切り分けて整理しておけば、デバッグ時に方向を見失いません。

もう一つ。デプロイに失敗しても慌てないこと。ログを段階別に見て、まず SSH 接続の問題かコマンド実行の問題かを特定しましょう。デバッグステップを一つ加えれば、問題はすぐに姿を現します。

次に深夜3時のデプロイ失敗に遭ったとき、あなたがもっと早く原因にたどり着けますように。

GitHub Actions による VPS デプロイの設定

GitHub Actions から SSH 経由で VPS にデプロイするまでの完全な手順

⏱️ 目安時間: 30 分

  1. 1

    ステップ1: SSH 鍵ペアを生成する

    ローカルでデプロイ専用の SSH 鍵を生成します:

    • ssh-keygen -t ed25519 -C "deploy@github" -f deploy_key
    • 公開鍵(deploy_key.pub)をサーバーの ~/.ssh/authorized_keys に追加
    • 秘密鍵(deploy_key)の内容を GitHub Secrets の SSH_PRIVATE_KEY に保存
  2. 2

    ステップ2: GitHub Secrets を設定する

    リポジトリの Settings → Secrets → Actions で追加します:

    • SSH_PRIVATE_KEY:秘密鍵の全内容
    • SERVER_HOST:サーバーの IP またはドメイン
    • SERVER_USER:SSH ユーザー名(root や ubuntu など)
    • DEPLOY_PATH:デプロイ先のパス
  3. 3

    ステップ3: Workflow ファイルを作成する

    .github/workflows/deploy.yml にデプロイ設定を作成します:

    • SSH 鍵設定ステップを追加(webfactory/ssh-agent-action)
    • known_hosts を設定して host verification の失敗を防ぐ
    • rsync でビルド成果物を転送
    • デプロイ後にサービス再起動コマンドを実行
  4. 4

    ステップ4: デプロイフローをテストする

    コードをプッシュして自動デプロイを発火させるか、手動で実行します:

    • 各ステップのログ出力を確認
    • SSH 失敗時は鍵の形式と known_hosts を確認
    • rsync 失敗時はパスと権限を確認
    • デバッグステップを追加して問題を切り分け

FAQ

GitHub Actions のデプロイで「Host key verification failed」が出たらどう解決しますか?
これは SSH が初めてサーバーへ接続する際に known_hosts 設定が無いことが原因です。2つの解決策があります:

• 方法1:ssh-keyscan でサーバーのフィンガープリントを取得し、SSH_KNOWN_HOSTS という Secret に保存する
• 方法2:workflow 内で手動で ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts を実行する

よりクリーンで安全な方法1を推奨します。
SSH 鍵はどこに置くべきですか?workflow ファイルに直接書いても大丈夫ですか?
絶対にダメです。秘密鍵は必ず GitHub Secrets に保存し、workflow からは `${{ secrets.SSH_PRIVATE_KEY }}` で参照します。鍵をハードコードするとコードリポジトリに残り、誰でも閲覧できる重大なセキュリティリスクになります。
Vercel、Cloudflare Pages、Netlify のうち個人プロジェクトにはどれが向いていますか?
Cloudflare Pages は無料枠が最も寛大で(帯域無制限、月500回ビルド)、アジアからのアクセス速度も安定しています。Vercel は Next.js プロジェクトとの相性が最高ですが、無料版の Serverless Functions には10秒の制限があります。Netlify はエコシステムが成熟しており、Form handling 機能が実用的です。

純粋な静的サイトなら Cloudflare Pages を優先しましょう。
ハイブリッドデプロイ構成にはどんなメリットがありますか?
フロントエンドをホスティングプラットフォームにデプロイすれば CDN 高速化と自動 HTTPS の恩恵を受けられ、バックエンドを VPS にデプロイすればプラットフォームの制約を受けず完全な制御権を持てます。データベースや定期タスクなど複雑なバックエンドサービスが必要なプロジェクトに向いています。

GitHub Actions のマルチ job workflow を使えば、両方へ同時にデプロイできます。
デプロイ失敗時にログが多すぎて読み切れません。素早く問題を特定するには?
ログを段階ごとに分けて、順番に切り分けます:

1. SSH 接続の問題 → Setup SSH と known_hosts のステップを確認
2. rsync 転送の問題 → パスの存在と権限を確認
3. サービス再起動の問題 → デプロイ先のファイル一覧を確認

失敗したステップの直後にデバッグ出力(ssh -v の詳細ログ)を追加すると、問題がすぐに見えてきます。
rsync の --delete パラメータにはどんなリスクがありますか?
--delete は転送先ディレクトリにあって転送元に無いファイルを削除し、両者を完全に同期させます。ただしパスを誤って設定すると、消してはいけないものまで消えてしまう恐れがあります。

初回デプロイでは --delete を付けず、正しいことを確認してから有効化するのがおすすめです。あるいは --delete-excluded で除外対象のファイルだけを削除する手もあります。

4分で読めます · 公開日: 2026年4月7日 · 更新日: 2026年6月8日

コメント

GitHubアカウントでログインしてコメントできます