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

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/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

このactionが自動的にssh-agentを起動し、鍵を読み込みます。便利です。

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

SSHで初めてサーバーに接続する際、このホストを信頼するかどうか尋ねられます。CI環境では対話的な質問に対応できないため、事前にサーバーのフィンガープリントをknown_hostsに追加する必要があります。

2つの方法があります:

方法1:actionを使って自動追加

- name: Add server to known hosts
  uses: webfactory/[email protected]
  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アプリをデプロイする場合、サービスを再起動する必要があります。

私はPM2を使ってNodeプロセスを管理しています。デプロイ後に次を実行します:

- 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秒)。これを超えるとタイムアウトします。

純粋な静的サイトやシンプルな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はあまり使っていませんが、老舗のホスティングプラットフォームで、エコシステムは成熟しています。

デプロイ設定は同様:

- 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、バックエンドはデータベースに接続し、cronジョブを実行する必要がある……。

このような場合、ハイブリッドデプロイが最適解かもしれません。

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

典型的なアーキテクチャ:

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

メリットは、それぞれの利点を活かせること:

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

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

1つのworkflowで2箇所にデプロイ:

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/[email protected]
        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/[email protected]
        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ログインユーザー名通常rootまたはubuntu
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」ステップを確認
    • 失敗した場合、対象パスにファイルがあるか確認

1つのテクニック:失敗したステップの後にデバッグ出力を追加。

- 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つのステップを明確に分けておけば、トラブルシューティングの際に混乱しません。

もう1つ:デプロイ失敗時に慌てないこと。ログをセグメントごとに見て、SSH接続の問題かコマンド実行の問題かを先に特定する。デバッグステップを追加すれば、問題はすぐに明らかになります。

次回、深夜3時にデプロイが失敗しても、より早く原因を見つけられることを願っています。

GitHub Actions VPSデプロイパイプラインを設定

GitHub Actionsの自動デプロイをVPSに設定する手順、SSH鍵、rsync転送、サービス再起動を含む

⏱️ 目安時間: 30 分

  1. 1

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

    GitHub Actions専用のSSH鍵をローカルで生成:

    ```bash
    ssh-keygen -t ed25519 -C "github-actions" -f github-actions-key
    ```

    2つのファイルが生成されます:
    - github-actions-key(秘密鍵)→ GitHub Secretsに追加
    - github-actions-key.pub(公開鍵)→ サーバーの ~/.ssh/authorized_keys に追加
  2. 2

    ステップ2: GitHub Secretsを設定

    リポジトリの Settings → Secrets → Actions で、以下のSecretsを追加:

    - SSH_PRIVATE_KEY:秘密鍵ファイルの完全な内容(BEGIN/END行を含む)
    - SERVER_HOST:サーバーのIPまたはドメイン
    - SERVER_USER:SSHログインユーザー名(rootやubuntuなど)
    - DEPLOY_PATH:デプロイ先のパス(例:/var/www/html)

    機密情報は絶対にworkflowファイルに書かないこと。
  3. 3

    ステップ3: workflowファイルを作成

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

    ```yaml
    name: Deploy to VPS
    on:
    push:
    branches: [main]
    jobs:
    deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Setup SSH
    uses: webfactory/[email protected]
    with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
    - name: Add server to known hosts
    run: ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts
    ```
  4. 4

    ステップ4: ファイル転送を設定

    rsyncを使ってビルド成果物を転送:

    ```yaml
    - name: Deploy files
    run: |
    rsync -avz --delete --exclude 'node_modules' --exclude '.git' ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}
    ```

    主なパラメータの説明:
    - -avz:アーカイブモード、詳細出力、圧縮転送
    - --delete:転送先の余分なファイルを削除
    - --exclude:転送から除外するディレクトリ
  5. 5

    ステップ5: デプロイ後コマンドを追加

    Node.jsアプリはサービスの再起動が必要:

    ```yaml
    - name: Restart application
    run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} "cd /var/www/app && pm2 restart all --update-env"
    ```

    --update-envパラメータは環境変数を再読み込みします。設定に変更があるデプロイに適しています。

FAQ

GitHub ActionsでVPSにデプロイする際、SSH接続に失敗する場合はどうすればいい?
以下の順序で調査:

1. 秘密鍵の形式を確認:GitHub Secretsの秘密鍵に完全なBEGIN/END行が含まれているか
2. 公開鍵の設定を確認:公開鍵がサーバーの ~/.ssh/authorized_keys ファイルに追加されているか
3. known_hostsを確認:ssh-keyscanコマンドでサーバーのフィンガープリントを事前に追加
4. デバッグステップを追加:ssh -v で詳細なログを出力し、問題を特定
rsyncとscpの違いは?デプロイにはどちらを使うべき?
rsyncを推奨、理由:

- 増分転送:変更されたファイルだけを転送し、帯域幅と時間を節約
- 除外機能:node_modulesなど不要なディレクトリをスキップ可能
- 削除同期:--deleteパラメータで転送先の余分なファイルを削除
- レジューム機能:ネットワーク中断後に再開可能

scpは少数のファイルを一度に転送する場合に適しています。
ホスティングプラットフォーム(Vercel/Cloudflare Pages)とVPSはどう選ぶ?
選択のアドバイス:

- 純粋な静的サイト:ホスティングプラットフォーム、設定が簡単でCDN高速化
- シンプルなAPI:ホスティングプラットフォームで十分、実行時間制限に注意(Vercel無料版は10秒)
- 複雑なバックエンド:VPS、完全な制御権があり、プラットフォームの制限を受けない
- ハイブリッドアーキテクチャ:フロントエンドホスティング + バックエンドVPS、それぞれの利点を活かす
GitHub Secretsの環境変数をサーバー上で使用するには?
2つの方法があります:

方法1:SSHコマンドで渡す
```yaml
ssh user@host "pm2 restart app --update-env DB_URL=${{ secrets.DB_URL }}"
```

方法2:サーバー上で.envファイルを使用
```yaml
- name: Update .env
run: |
ssh user@host "echo 'DB_URL=${{ secrets.DB_URL }}' > /var/www/app/.env"
```

方法2を推奨、より安全で管理しやすいです。
デプロイ失敗時に問題を素早く特定するには?
3ステップで調査:

1. SSH接続の問題:Setup SSHとknown_hostsステップのログを確認
2. ファイル転送の問題:Deploy filesステップを確認、パスと権限をチェック
3. サービス起動の問題:再起動コマンドの出力を確認

デバッグステップを追加して詳細情報を出力:
```yaml
- name: Debug
if: failure()
run: ssh -v user@host "ls -la /deploy/path"
```
ハイブリッドデプロイ(フロントエンドホスティング + バックエンドVPS)を実現するには?
1つのworkflowで複数のjobを使用:

1. build job:プロジェクトをビルドし、upload-artifactで成果物を保存
2. deploy-frontend job:成果物をダウンロードし、ホスティングプラットフォームにデプロイ
3. deploy-backend job:SSHでVPSにアクセスし、バックエンドサービスをデプロイ

重要なポイント:
- needsで実行順序を制御
- artifactでjob間でファイルを受け渡し
- フロントエンドとバックエンドの環境変数を分けて管理

5 min read · 公開日: 2026年4月7日 · 更新日: 2026年4月8日

コメント

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

関連記事