ポート開放なし・自宅サーバーへの GitHub Actions 自動デプロイ手順

ポート開放なし・自宅サーバーへの GitHub Actions 自動デプロイ手順

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。

自宅サーバーで動かしている個人サイトに、git push だけで自動デプロイを仕込みたい。ポート開放なしの構成では使えるCI手法が絞られますが、GitHub Actions のセルフホスト runner なら条件に合います。この記事では、Cloudflare Tunnel で公開している Next.js + systemd 構成を想定し、runner のインストールからワークフロー作成・よくある詰まりポイントまでを順に書きます。

セルフホスト runner を選ぶ理由

GitHub-hosted runner を使った SSH デプロイは、デプロイ先にインバウンドポートを開ける必要があります。Cloudflare Tunnel 経由で公開している構成ではポート開放をしていないため、外から SSH で入る経路がありません。

セルフホスト runner は仕組みが逆で、サーバー側で動くプロセスが GitHub にポーリングして仕事を取りにいきます。インバウンドポートは不要で、アウトバウンドの HTTPS(443番)が通れば動作します。

GitHub-hosted runner セルフホスト runner
インバウンドポート 必要(SSH 等) 不要
実行環境 GitHub のクラウド 自分のサーバー
課金(執筆時点) 月2,000分まで無料(無料プランの場合。詳細はGitHub公式ドキュメントで最新値を確認) 無料
ビルド速度 転送コストあり ローカルビルドで速い

事前準備

  • GitHub リポジトリ(パブリック・プライベートどちらも対応)
  • Linux サーバー上に runner 専用の非 root ユーザーを用意する
  • Node.js・npm がサーバーにインストール済み
  • アプリが systemd サービスとして登録済みで、サービス名を把握している

systemd サービスの登録がまだであれば、systemdサービスを自動起動させる設定手順を先に参照してください。

セルフホスト runner のインストール

GitHub リポジトリの Settings > Actions > Runners > New self-hosted runner を開きます。OS に Linux、アーキテクチャに x64(または ARM64)を選ぶと、そのページ専用のコマンドが表示されます。以下はコマンド形式の例です(バージョン番号はページ表示のものを使ってください)。

mkdir ~/actions-runner && cd ~/actions-runner
curl -o actions-runner-linux-x64-2.x.x.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.x.x/actions-runner-linux-x64-2.x.x.tar.gz
tar xzf ./actions-runner-linux-x64-2.x.x.tar.gz
./config.sh --url https://github.com/<owner>/<repo> --token <TOKEN>

<TOKEN> はページに表示される一時トークンです(有効期限はGitHubの仕様により短時間です。ページを開いたままにして続けて実行してください)。config.sh を実行するとランナー名・作業ディレクトリを対話的に設定できます。デフォルトのまま Enter で進んで問題ありません。

次に、runner を systemd サービスとして登録して常駐させます。

sudo ./svc.sh install
sudo ./svc.sh start

actions.runner.<repo>.<runner-name>.service という systemd ユニットが作られ、サーバー再起動後も自動で起動します。状態確認は次のコマンドです。

sudo systemctl status actions.runner.<repo>.<runner-name>.service

Active: active (running) と表示されれば準備完了です。

ワークフローファイルの作成

リポジトリのルートに .github/workflows/deploy.yml を作ります。

name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: self-hosted
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NODE_ENV: production

      - name: Restart service
        run: sudo systemctl restart your-app.service

runs-on: self-hosted が、先ほど登録したランナーで実行することを指定するキーです。actions/checkout@v4 でコードをチェックアウトし、ビルドしてから systemd サービスを再起動します。

環境変数・シークレットの扱い

.env や .env.local をリポジトリに含めるのは避けます。GitHub の Settings > Secrets and variables > Actions にシークレットを登録し、ワークフローから参照します。

      - name: Write .env.local
        run: |
          cat > .env.local << 'EOF'
          DATABASE_URL=${{ secrets.DATABASE_URL }}
          NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }}
          PAYJP_SECRET_KEY=${{ secrets.PAYJP_SECRET_KEY }}
          EOF

シークレットの値はログ上でマスクされます。ただし cat .env.local を実行するステップをワークフロー内に書くと値が漏れるため、確認目的のダンプは避けてください。

runner ユーザーに sudo を許可する

systemctl restart your-app.service を runner ユーザーが実行できるよう、sudoers を設定します。/etc/sudoers.d/ に専用ファイルを作るのが管理しやすい方法です。

sudo visudo -f /etc/sudoers.d/github-runner

ファイルの内容(ユーザー名とサービス名は実態に合わせてください):

runner-user ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart your-app.service

NOPASSWD: ALL は権限過多です。再起動を許可するサービスを1行ずつ列挙する形にとどめてください。

つまずきやすいポイント

npm: command not found が出る

セルフホスト runner はシェルのログインプロファイルを読み込まないため、PATH に Node.js が入っていないことがあります。ワークフロー内で actions/setup-node アクションを使うと、node と npm のパスが自動で追加されるため、これを経由するのが確実です。

ビルドが古いキャッシュを引きずる

同じサーバーで runner が動くため、node_modules が残り続けます。npm install ではなく npm ci を使うと、node_modules を一度削除してから package-lock.json 通りにインストールするため、キャッシュの食い違いを防げます。

systemctl restart でタイムアウトする

Next.js の next start はプロセスが起動してからリクエストを受け付けるまでに数秒かかることがあります。systemd ユニットに TimeoutStartSec=60 を設定しておくと、起動待ちでタイムアウトして CI が失敗するケースを防げます(環境によって必要な値は異なります)。

Prisma マイグレーションを CI に含めるか

prisma migrate deploy をワークフローに入れると、プッシュのたびにマイグレーションが走ります。本番 DB への影響が大きいため、マイグレーションは手動実行を原則にして CI からは外す判断も合理的です。チームや運用スタイルによって異なります。

main へのプッシュが多い場合のキュー詰まり

セルフホスト runner は1台のサーバーで1ジョブずつ直列処理します。短い間隔で複数のプッシュが来ると後続ジョブが待ちに入ります。concurrency オプションを設定すると古いジョブをキャンセルして最新だけを実行できます。

concurrency:
  group: deploy
  cancel-in-progress: true

まとめ

セルフホスト runner を使えば、ポート開放なしの自宅サーバーに git push だけで自動デプロイを組めます。押さえるべきポイントは3つです。

  1. runner を systemd サービスとして常駐させ、再起動後も自動起動させる
  2. シークレットはリポジトリに含めず GitHub Secrets で管理する
  3. sudo の権限は再起動するサービス名だけに絞って最小化する

まず自分のリポジトリで runner を登録して、runs-on: self-hosted の空ジョブ(run: echo "hello")を動かすところから始めると動作確認が楽です。ビルドとデプロイのステップはそのあとに追加していきましょう。

よくある質問

GitHub Actions セルフホスト runner はポート開放なしで使えますか?

使えます。セルフホスト runner はサーバー側からGitHubにポーリングする仕組みのため、インバウンドポートは不要です。アウトバウンドのHTTPS(443番)が通れば動作します。Cloudflare Tunnel 経由で公開している構成にも対応できます。

runner のユーザーに sudo を与えるのは危険ではないですか?

NOPASSWD: ALL のように全コマンドを許可すると危険です。sudoers では `NOPASSWD: /usr/bin/systemctl restart your-app.service` のように再起動を許可するサービスだけを列挙する形にとどめると、権限を最小化できます。

セルフホスト runner と GitHub-hosted runner の違いは何ですか?

GitHub-hosted runner はGitHubのクラウドで動き、月2,000分まで無料(執筆時点)。セルフホスト runner は自分のサーバーで動き、ポート開放が不要で課金もありません。自宅サーバー構成ではセルフホスト runner が適しています。

Prisma のマイグレーションは CI に入れるべきですか?

本番DBへのマイグレーションをCIに含めると、プッシュのたびに自動実行されて誤操作のリスクが高まります。マイグレーションは手動実行を原則にして CI から外す運用が安全です。環境や体制によって判断が異なります。

npm install と npm ci の違いは何ですか?

npm ci は node_modules を一度削除してからpackage-lock.jsonの内容通りにインストールします。npm install はpackage.jsonを元に差分更新するためキャッシュの食い違いが起きやすく、CI環境ではnpm ciの使用が推奨されています。