Bot mmnot 環境構築・インフラ 開発ログ

🛠️開発記録#239(2025/5/31)Self-hosted Runner問題を超えて 〜安定運用までの軌跡〜

はじめに

「Self-hosted Runner による GitHub Actions の運用は、自由で強力で、そして時に不親切だ。」

これは、私が仮想通貨Botの開発において、Runnerと真っ向から向き合った時間を経て感じたことです。
テストを自動化したい。デプロイも監視も整えておきたい。――そんな開発者として当然の願いから導入したSelf-hosted Runnerでしたが、最初は「ジョブが走らない」「ログが残らない」「Runnerが拾わない」といった謎の現象に悩まされ続けました。

結果的に私は、自分自身でRunnerの挙動を構造から分解・理解し、設定し直し、そして「安定して拾ってくれるRunner」を構築するに至りました。この記事では、その過程で得られた知見と、技術的なポイントだけでなくどこで迷いやすいかという心理面も含めて、これからRunnerを導入する方に向けてお伝えしたいと思います。


なぜ Self-hosted Runner を選んだのか

私がSelf-hosted Runnerを選んだ最大の理由は、「自分のBotを、自分のマシンと制御の中で動かしたかったから」です。
仮想通貨Botの開発では、テスト環境と本番環境を可能な限り近づけること、そしてトレードが発生する実際のプロセスを常に監視下に置くことが極めて重要です。

GitHubが提供するホスト型のActionsランナーでは、ジョブごとにマシンが切り替わり、毎回クリーンな環境で再起動されるため、Botのような常駐プロセスの監視や継続稼働が前提となる開発には向いていませんでした。

そこで選んだのが、Self-hosted Runner。
自分のマシン上で、特定のジョブを常時受け取り、状態を持ったまま継続動作できる環境――それが、私のBot運用にとって理想的だったのです。


初心者がハマりがちなポイント

実際に使ってみてわかったのは、Self-hosted Runnerは仕組みを理解していないと容赦なく無視される存在だということでした。

  • 「ジョブがキューに入ってるのにRunnerが拾わない」
  • self-hostedって書いたのに動かない」
  • runs-onでラベルを付けたけどマッチしない」
  • 「Runnerを再起動しても復旧しない」

こうした問題に直面したとき、ログは無言で、GitHubは何も教えてくれません。
特に「ラベル」と「登録スコープ(リポジトリ単位か、組織単位か)」の理解が浅いまま進めると、Runnerがジョブを永遠にピックアップしてくれないという状態に陥ります。

また、PM2などのプロセスマネージャーと組み合わせることで常駐運用は可能になりますが、「止まったらどうする?」「再起動時に立ち上がる?」「ログは追える?」といった実運用を見越した整備をしなければ、結局またトラブルの連鎖が起きるのです。


今回の記事で伝えたいこと

この記事で私が伝えたいのは、単なるSelf-hosted Runnerの使い方ではありません。
それは公式ドキュメントを読めばわかりますし、最小限の設定であれば誰でもすぐにできます。

そうではなく、「なぜ動かないのかがわからないRunner」とどう向き合い、どう設計していけばいいのか?
そして、「どうすれば“安心して任せられるRunner”に進化させられるのか?」という、開発と運用の狭間で揺れながら構築してきた実践知を共有したいのです。

私がRunnerに苦しみ、悩み、試し、やがて「いつものように緑のランプが灯る」日常を取り戻すまでの流れは、これからBot開発や常駐システムの運用に挑む人にとって、きっと役に立つはずです。

この記録が、誰かのRunnerトラブルを減らし、環境構築の迷いを減らし、開発の集中を取り戻す手助けになれば幸いです。

1. 最初に立ちはだかったRunnerの壁

Self-hosted Runner を立ち上げて最初に遭遇したのは、「ジョブが拾われない」という沈黙のバグでした。

GitHub Actions のワークフローは問題なく push され、Actions タブにもトリガーされたジョブが表示されている。ところが、ステータスはずっとこう表示され続けるのです。

Waiting for a runner to pick up this job...

まるで誰にも見つけられずに放置された配達物のように、ジョブはQueueに溜まり、時が過ぎていくだけ。

初めてこのメッセージを見たとき、私も「なぜ? Runnerは立ち上がってるのに?」という混乱に陥りました。ログは出ない。エラーもない。ただ動かない


1-1. “ジョブが拾われない”問題

この問題には、いくつもの原因が絡み合っていました。

  • RunnerがリポジトリAにしか登録されていないのに、リポジトリBのジョブを拾おうとしていた
  • runs-on: に指定したラベル(例:self-hosted, healthcheck)が、Runner側に存在していなかった
  • Runnerプロセスが pm2 で動いているつもりが、実はクラッシュしていた

とにかく、どこが問題なのかをRunnerが一切教えてくれないというのが最大の難所でした。

当時の私は、GitHub Actions がデフォルトで提供してくれる ubuntu-latest 環境のように、Self-hosted Runnerも「指定すれば勝手に拾ってくれるもの」だと勘違いしていたのです。


1-2. Waiting for a runner to pick up this job...の恐怖

このメッセージは開発者に対して何も言わずに不在を知らせるという意味で、本当に厄介です。

しかもこの状態でRunner側をいくら再起動しても、ジョブは動きません。
重要なのは「Runnerが登録されているリポジトリ(または組織)」と、「ワークフローで指定されているラベル」の一致であり、それが合致しない限り、GitHub側はRunnerを無視し続けます。

問題が「どこで起きているのか」を可視化できない状況は、開発者を精神的に疲弊させます。


1-3. ラベルと登録スコープの理解不足

この問題を突き詰めていくと、登録スコープとラベルの2つの構造を理解していなかったことにたどり着きました。

  • 登録スコープ
    Runnerがリポジトリ単位で登録されていれば、そのリポジトリのジョブしか拾わない。
    一方、組織単位で登録されていれば、組織内すべてのリポジトリからジョブを受け取れる。
  • ラベル
    runs-on: [self-hosted, my-runner] などと書かれていると、そのラベルを持っていないRunnerはスルーされる。
    たとえ self-hosted を名乗っていても、ラベルが足りないとマッチしない

この構造を知らずに、私はRunnerを何度も再起動し、PM2のログを眺め、ワークフローを手動トリガーしては落胆していました。

でも、この「構造を理解する」ことで、ようやく私はRunnerという“沈黙の存在”とコミュニケーションを取れるようになったのです。

2. 構造を理解して打開した

問題を乗り越えるきっかけは、「とにかく構造を図解してみる」ことでした。
ジョブがRunnerに届かない、Runnerが無反応──その背景には、GitHub Actionsという仕組みそのものの構造的な制限がありました。
私はこの“仕組みの設計意図”を理解することで、ようやく打開への道筋を描くことができたのです。


2-1. リポジトリ登録 vs 組織登録

最初に気づいたのは、Runnerの登録先スコープの違いです。

  • リポジトリ単位で登録されたRunnerは、そのリポジトリのワークフローしか拾いません。
  • 一方で、組織単位で登録されたRunnerは、組織内すべてのリポジトリに属するジョブを拾える設計になっています。

この構造を知らずに、私は「MMbot用に立ち上げたRunnerが、runner-configのHealth Checkを拾わないのはなぜ?」と悩み続けていたのです。
実際には、Runnerが別のリポジトリにしか登録されていないので、物理的には生きていても、論理的には無視されていただけでした。

解決策は、Runnerを複製し、それぞれのリポジトリ用に別々に登録すること。
actions-runner(MMbot用)と actions-runner-hc(Health Check用)という2つのプロセスに分離し、各々を別のリポジトリに登録し直しました。


2-2. Self-hosted Runner のラベル構造

次に理解すべきだったのが、ラベルのしくみです。

GitHub Actions では、ワークフロー内で以下のように Runner を指定します。

runs-on: [self-hosted, macos, x64]

このとき、Runner 側が すべての指定されたラベルを持っていなければ、マッチしません。
self-hostedって書いたのに拾われないじゃん!」というときは、たいてい ラベルが足りていないのです。

私はここで、「ラベルはRunnerの身分証明書みたいなものだ」と理解しました。
その身分証に "healthcheck" と書かれていなければ、runs-on: [self-hosted, healthcheck] のジョブは絶対に来ない。
この構造を理解してからは、「ラベルの付け方を変えるだけでジョブを切り分けられる」という運用の自由さに気づきました。


2-3. Runnerの「役割と用途」に名前をつけるという発想

最後に効いたのは、「Runnerに“用途”を名前として明示する」というシンプルな工夫でした。

たとえば:

  • actions-runneractions-runner-e2e(Live E2Eテスト専用)
  • actions-runner-hc(Health Check専用)

この命名のルールがあるだけで、PM2の管理も、ログの確認も、トラブル時の切り分けも劇的に楽になります
それだけでなく、運用するチーム内(私の場合はAI)や、未来の自分にとっても「このRunnerは何をしているのか」が一目でわかるようになるのです。

この「構造を理解し、意味のある名前をつけ、構成を明示的に分ける」という一連の作業によって、私はようやく、Runnerという無言の存在を“制御可能な仲間”に変えることができたと感じました。

3. 安定運用のために整えたもの

Self-hosted Runner がジョブを正常に拾うようになったからといって、それですべてが終わりではありません。
むしろここからが本番でした。安定して動き続けるRunnerをどう維持するか?
Bot開発の基盤としてRunnerを使い続けるには、「壊れない仕組み」「壊れたときに止まる仕組み」「そして復旧できる仕組み」が必要でした。

私はこの段階で、「環境が安定していれば、Botのロジック改善に集中できる」という構造の強さを、身をもって理解することになります。


3-1. Runner名の用途別命名と複数プロセス管理(PM2運用)

まず取り組んだのは、Runnerの用途別命名と、複数Runnerの並列運用です。

  • actions-runner-e2e → Live E2E テスト専用
  • actions-runner-hc → Health Check ワークフロー専用

これらを PM2 で管理することで、プロセスの可視性が高まりました。

pm2 list

このコマンド一つで、どのRunnerが「online」か、「停止中」かがすぐにわかる。
また、pm2 save によって状態を永続化し、macOS再起動後もRunnerが自動復元されるようにしたことで、人的オペレーションなしで稼働し続けられる環境が整いました。


3-2. 健全性チェックとSlack通知

Runnerが動いているかを可視化するために、30分おきに実行される健全性チェックワークフローを導入しました。

  • .github/workflows/runner_healthcheck.yml を作成
  • 実行失敗時には Slack Webhook で即座に通知
  • テストとして意図的にRunnerを止めて失敗を発生させ、通知が飛ぶことも確認済み

この仕組みによって、「何かあっても通知が来るから、安心して離席できる」という状態になりました。
通知が来ない=異常がない というシンプルなルールは、精神的な負荷を大きく下げてくれます。


3-3. Docker/Botと連携した自動停止・再起動機構

MMbotはDockerコンテナ上で稼働しています。
そのため「Botが異常停止した」「ログが止まった」「PnLが更新されない」などの状況に対応するために、watchdog.sh のような監視スクリプトを設計しました。

  • 異常条件を検出(エラーログの連続/PnLファイルの静止など)
  • 条件を満たしたら自動で docker restart mmbot
  • 再起動回数に上限を設け、無限ループは防止
  • Slack通知と連携して、オペレーターに状況を伝える

まだこれは導入初期ですが、自動復旧と安全停止の両立は長期稼働Botには必須の機構です。
「死なないBot」ではなく、「死んだときに止まれるBot」こそが信頼されるBotなのだと実感しました。


3-4. .env, .gitignore, docs/ の整備による保守性向上

最後に整えたのは、日々の運用に関わるファイル構成とルールです。

  • .env.production, .env.testnet, .env を分離
  • .gitignore によって .env.credentials* を誤ってコミットしないように保護
  • docs/runner_setup.md に手順と注意事項をまとめ、1週間後の自分でも再現できるようにした

一見地味ですが、これらの整備があるだけで、新しいRunnerを立ち上げる作業時間が半分以下になりました。
開発と保守のどちらにも時間を割くためには、「忘れても安全な設計」が必要だということをここでも再確認しました。


このようにして、私は「ジョブを拾う」だけでなく、「動き続ける」「壊れても立ち直る」Runnerを手に入れることができました。

4. 開発者として何を得たか

Self-hosted Runner の運用は、一見すると「地味な裏方作業」のように見えます。
コードを書いているわけでもない。トレード戦略を考えているわけでもない。
しかし私にとってこの期間は、開発者としての根本的な姿勢を見直し、鍛え直す時間になりました。


4-1. システムが安心して動き続けることの価値

最も大きな実感はこれでした。
**「緑のランプが灯っているだけで、脳のメモリが空く」**ということ。

開発者として何かに取り組むとき、目の前のロジックに集中するためには「環境の安定」が不可欠です。
Runnerが不安定だと、戦略に集中できない。
通知が来ないと、安心して離席できない。
Botが止まると、再起動するたびに細かい判断が必要になる。

このストレスがなくなるだけで、圧倒的に深く、長く、クリアに考えられるようになります。
これは私にとって、非常に大きな体験でした。


4-2. 「壊れない環境」はBotの戦略開発を加速させる

MMbot、FRbot、アビトラbot――いずれのBotも、動くことが目的ではなく、勝つことが目的です。
しかし勝つためには、まず「止まらずに動く」必要があります。

私がRunnerの安定運用にこだわったのは、戦略改善の反復サイクルを絶え間なく回し続けるためです。
実験→観察→修正→再テスト。
これが1日10回できるか、1回しかできないかで、Botの成長スピードは天と地ほど違います。

つまり、Runnerの整備は単なる保守作業ではなく、Botの“育成スピード”そのものを左右する要素だと、私は考えるようになりました。


4-3. 自動化と観察の視点を手に入れた

そしてRunnerを通じて得たもう一つの視点は、**「自動化とは手放すことではなく、観察を減らすこと」**という感覚です。

Slack通知ひとつ。ログ整備ひとつ。
それだけで「何を見なくてよくなるか」が増えていき、最終的にはBotの“意思”を見るだけで状況が理解できる状態に近づいていきます。

私の開発環境のテーマの一つは、ほとんど手をかけずに日々動かすことです。
毎朝起きて、「ランナーが緑なら今日は開発を進めていい」「赤なら今は環境に集中する」――そういう判断軸を、自分以外の何かに委ねられるようになってきました。

これは、専業Bot開発者としての“時間の使い方”を劇的に変えてくれています。


おわりに

Self-hosted Runnerの導入は、Botの強さを直接左右するものではありません。
しかし、Botの強さを継続して磨き続けられる環境をつくるという点で、私にとって欠かせない基盤でした。

「なぜジョブが拾われないのか」
「Runnerは本当に動いているのか」
そんな“黙ったまま動かない仕組み”と向き合う時間は、私に「構造を理解して支配する」という視点をくれました。

これからも私は、MMbotを中心に、FRbot、アビトラbotといったさまざまなBotの開発を進めていきます。
その土台として、自分が手で整えたRunnerたちが日々静かに働いてくれていることに、深い安心と誇りを感じています。

この記録が、あなたの開発にも、ひとつのヒントや背中押しになれば幸いです。

-Bot, mmnot, 環境構築・インフラ, 開発ログ