本文へスキップ
Deployboardv0.0.3

macOS · コマンド一つ · 127.0.0.1

Launchd に dashboard はない。自分の launchd service をブラウザで管理する console で、Prometheus metrics も出す。

コマンド一つで入る。ブラウザで status、log、alert を見る。

$brew install --cask francistse/tap/deployboard
導入コマンド一つブラウザstatus · log · alert出力Prometheus /metrics
127.0.0.1:9410 · Deployboard
Deployboard
自分の launchd service を管理する console
SystemLightDark⚙ 設定
16 オン · 2 オフ自前 16 · その他 1 · ノイズ 528書き込みモードhttp://127.0.0.1:9410
ラベルを検索…
すべて (545)実行中 (163)スケジュール済み (2)完了 (0)停止 (239)エラー (139)オフライン (1)無効 (1)
ラベルPID終了再起動操作
サンプルアプリ3105 件
com.example.app.api自前記載あり
730872h 14m028
🔔再起動ログ
com.example.app.web自前自動(パス)↻ 再起動多発
730916d 3h01207↻
🔔再起動ログ
com.example.app.collector自前自動(パス)
7310418m021
🔔再起動ログ
com.example.worker.nightly自前記載あり
—13
🔔開始ログ
com.example.backup.weekly自前自動(パス)次回 · Sun 03:00
—00
🔔開始ログ
運用0012 件
com.example.worker.old自前記載あり⏹ 退役 12日
——0
有効化ログ
com.example.legacy.agent自前自動(パス)
—00
開始ログ
GET /api/eventstext/event-stream5秒ごとに配信

自前・その他・ノイズをクリック。それから /metrics と Telegram 通知。

理由

launchctl は全部出す。だから、何も見えない。

標準のツールが答えるのは「何が読み込まれているか」であり、「何をデプロイし、それは健全か」ではありません。ユーザードメインで絞り込んでも同じです。動いている Mac では、「自分の」ジョブに見えるものの大半が Dropbox、Google のアップデータ、Spotify、Tailscale、ブラウザのヘルパーです。1,207 回再起動しているジョブは、その真ん中に埋まっています。

Deployboard は、明示的な台帳から始めます。自分で書く許可リストと、パスからの導出です。plist のログパス、作業ディレクトリ、program、引数のいずれかがプロジェクトツリーの中にあれば、そのジョブは自動的に自分のものです。それ以外はシステムの用事であり、ワンクリックで届きますが、邪魔にはしません。

Deployboard なし
  • launchctl list — 545 行。どれが自分のものか分からない
  • 静かに 1,207 回再起動した開発サーバーが、正常に見える
  • 意図して無効にした plist が、いつまでも障害に見える
  • 履歴がない。「これは平常か?」に答えがない
  • 何かが死んだことに、偶然気づくまで分からない
Deployboard あり
  • 自前 / その他 / ノイズ — 初期表示は自分のデプロイ
  • launchctl print の runs(再起動回数)。閾値を超えると印が付く
  • disabled は状態であり、エラーではない。退役は退役のまま
  • Prometheus の /metrics。Grafana で「いつから」が見える
  • 遷移のときに Telegram 通知。アプリごと、クールダウン付き

状態

状態は七つ。その一つは「自分で退役させた」。

多くのツールは running / stopped / error に潰してしまいます。すると、何ヶ月も前に自分で止めた plist が、永遠に障害として表示されます。Deployboard は launchd が実際に区別している状態を保ち、launchctl print-disabled を読むので、意図も答えの一部になります。

実行中

PID > 0 — launchd print も生存に同意している

スケジュール済み

PID = 0、正常終了、スケジュールあり、直近のログなし

完了

PID = 0、正常終了、ログの mtime が --recent-window 以内

停止

PID = 0、正常終了、スケジュールなし、直近のログなし

エラー

直前の終了ステータスが非ゼロ

無効

launchctl print-disabled が disabled、生きている pid はない — 退役であり、故障ではない

オフライン

plist ファイルはあるが、launchctl list がラベルを返さない

このフォークが足すもの

本家が答えるのは「何が読み込まれているか」。こちらが答えるのは「何をデプロイしたか」。

追加は四つ。どれも launchctl 自身の出力の上に乗せてあり、ジョブがどのベンダーのものかを推測しません。

01

launchctl print を使う。launchctl list ではない。

launchctl list には再起動回数も disabled フラグもありません。Deployboard は runs と pid を launchctl print から、意図を launchctl print-disabled から読みます。回転が見え、意図して退役させたジョブが故障に見えなくなります。

02

台帳は、自分で決める。

自前 / その他 / ノイズ。各行に出処があります(記載あり、自動(パス)、未分類)。分類の操作は config.json に書き戻され、約 2 秒でホットリロードされます。新しいアプリを教えるのに再起動は要りません。

03

launchd のための Prometheus メトリクス。

deployboard_job_up、_runs、_last_exit_code、_disabled に加え、IPv4/IPv6 のポートプローブがあります。ダッシュボードが言えるのは「今落ちている」までです。時系列なら「03:10 から回転し始めた」と言えます。

04

アプリごとの切替が付いた Telegram 通知。

通知するのは遷移だけです。error、offline、ポート到達不能、再起動ストーム、そして復旧。アプリごとのスイッチ、クールダウン、静音時間があり、ボットのトークンは macOS のキーチェーンに置きます。ファイルには書きません。

本家から、そのまま残しているもの
ログの追尾

ジョブごとの stdout と stderr。最大 10,000 行、ライブで絞り込めます。

六つの診断

終了コード、program の存在、実行権限、plist の所有者、plist のパーミッション、ログパス。

SSE。ポーリングではない

GET /api/events が、ジョブ一覧全体を 5 秒ごとに送ります。

ワンクリック操作

launchctl 経由の start / stop / reload。確認ダイアログ付きです。

JSON API

UI が使っているのと同じ面。文書化されていて、curl で触れます。

依存ゼロ

Go のバイナリ一つ。フロントは go:embed。常駐デーモンもテレメトリもありません。

メトリクス

この領域で、唯一の launchd exporter。

Prometheus には、UI と同じポートを向けてください。ラベルのカーディナリティは有界です。一度も終了していないジョブに last-exit の系列は出さず、ポートを公開しているジョブにだけ probe の系列を出します。一部の開発サーバーは IPv6 の ::1 にしかバインドしないため、プローブはそこも試します。

GET /metrics
# HELP deployboard_job_up 1 when the launchd job is running.
# TYPE deployboard_job_up gauge
deployboard_job_up{label="com.example.app.api",category="ours",group="Example App"} 1
deployboard_job_runs{label="com.example.app.web",category="ours",group="Example App"} 1207
deployboard_job_disabled{label="com.example.worker.old",category="ours"} 1
deployboard_job_probe_reachable{label="com.example.app.api",port="8001"} 1
deployboard_job_probe_latency_seconds{label="com.example.app.api",port="8001"} 0.003
deployboard_jobs_total{category="ours"} 16
deployboard_jobs_total{category="noise"} 528
promql
# restart churn, per job, over 6h
deriv(deployboard_job_runs{category="ours"}[6h]) * 3600
# anything of yours that is not running
deployboard_job_up{category="ours"} == 0

UI を配信しているのと同じプロセスからスクレイプできます。別の exporter も sidecar も要りません。出せない系列は省き、値は作りません。一度も終了していないジョブに last_exit の行はなく、ポートのないジョブに probe の行はありません。

アラート

Telegram に、一度だけ知らせます。

遷移ベースなので、フラッピングするジョブが通知を埋め尽くすことはありません。壊れたときに一通、再起動が続くときはレート制限された一通、戻ったときに一通です。復旧の通知は静音時間でも抑止しません。ボットのトークンは macOS のキーチェーンにあり、ダッシュボード自身が書き込みます。ファイルにも、ログ行にも、ページの応答にも出しません。

✈telegram · deployboard
🔴 RUN_STORM
Example App · com.example.app.web
状態: 実行中
再起動: 1207
2026-10-03 10:41:07 +0800
きっかけ種別
状態が error になるerror
plist はあるが未ロードoffline
ポートが応答をやめたunreachable
走査のあいだに runs が +25run_storm
running に戻ったrecovery
アプリごとのスイッチ、クールダウン、静音時間。復旧メッセージは静音時間に飲み込まれません。

API

UI が使っているものは、スクリプトからも使えます。

Web UI は /api/jobs と /api/events の利用者の一つです。curl も同じです。サーバーをバインドし、エンドポイントを向き、JSON を解析してください。

メソッドパス説明
GET/api/inventoryカテゴリとグループの件数、および各ジョブの分類方法
POST/api/inventory/classifyジョブを自前に固定 / 隠す / 自動に戻す
GET/metricsPrometheus の公開形式 — ジョブ、runs、終了コード、プローブ
GET/api/jobsオフラインの plist を含め、すべてのジョブを一覧
GET/api/jobs/{label}単一ジョブのスナップショット(status、pid、runs、disabled)
GET/api/alertsアプリごとの通知状態と、有効 / 無効の件数
POST/api/alerts/{label}通知の切替: {"enabled": false}
POST/api/alerts/{label}/testそのジョブのテスト通知を一通送る
GET/api/settings/telegramトークンは保存済み?チャット ID は?トークン本体は返さない
GET/api/jobs/{label}/logs?lines=200stdout と stderr を追尾(最大 10,000)
GET/api/eventsSSE ストリーム — ジョブ一覧全体を 5 秒ごとに配信
POST/api/jobs/{label}/reloadlaunchctl bootout + bootstrap(読み取り専用モードでは 403)

インストール

コマンド一つ。設定ファイル一つ。タブ一つ。

導入は3通りで同じバイナリ: Homebrew cask(ヒーローのコマンド)、Go なしで LaunchAgent まで入れる curl install-release.sh、クローンからの bash install.sh。sudo 不要。スクリプトは --dry-run と --uninstall 対応。

01 · インストール
$ brew install --cask francistse/tap/deployboard

Homebrew の cask でバイナリを入れるか、install-release.sh / install.sh で RunAtLoad + KeepAlive の LaunchAgent まで入れます。sudo 不要。スクリプトには --dry-run と --uninstall もあります。

02 · 自分のアプリを指定する
$ open ~/.config/deployboard/config.json

config.json の inventory.ours に名前空間を書くか、derive_roots にプロジェクトのフォルダを足して、plist のパスに分類させます。未分類のものは「その他」に出ます。UI でワンクリックすれば固定でき、config.json はホットリロードされます。

03 · 見るか、スクレイプするか

ダッシュボードは 127.0.0.1 で開きます。Prometheus から /metrics をスクレイプし、ベルでアプリごとの通知を切り替え、テーマをシステム、ライト、ダークから選べます。

7

状態の値

本家の六つ + disabled

2s

設定のホットリロード

再起動なしで新しいアプリを分類

12

Prometheus の系列ファミリー

ジョブ、runs、終了コード、プローブ

127.0.0.1

バインドする唯一のアドレス

クラウドなし、テレメトリなし、更新確認の通信なし

FAQ

フォークなら、答えておくべき質問。

本家とどういう関係か、も含みます。

フォーク元の launch-pilot と、何が違いますか?

本家ができることは、そのまま残しています。ジョブ表、ログの追尾、六つの診断、start/stop/reload です。Deployboard が足すのは、台帳ビュー(パス導出付きの自前 / その他 / ノイズ)、launchctl print による補強(runs と disabled)、Prometheus のエンドポイント、アプリごとに切り替えられる Telegram 通知、コマンド一つの macOS インストール、そして読み取り専用モードです。

どのジョブが自分のものか、どうやって分かりますか?

規則は二つで、この順です。まず自分で書く許可リスト(inventory.ours)。次に導出です。plist のログパス、作業ディレクトリ、program、引数のいずれかが derive_roots に挙げたディレクトリの中にあれば、そのジョブは自分のものです。plist が指しているものはすべて証拠なので、推測もドメインの経験則も使いません。

ジョブを間違えたらどうなりますか?

「その他」に出てきて、「未分類」と付きます。行から自動 / 自前 / 隠すを選べます。それが config.json に書き込まれ、サーバーは約 2 秒でホットリロードします。静かに隠すことはありません。launchd の全一覧は、常にチップ一つ先にあります。

このうち何かが、自分のマシンの外に出ますか?

出ません。HTTP サーバーは 127.0.0.1 にだけバインドし、分析も電話ホームもありません。プロジェクト全体が外に出せる通信は、明示的に有効にした Telegram メッセージだけです。トークンは自分のキーチェーンにあります。

通知なしで、メトリクスだけ使えますか?

使えます。通知の初期値は、自分のアプリではオン、それ以外ではオフです。アプリごと、カテゴリごと、または全体で止められます。/metrics と JSON API はそのまま動き、--read-only は変更を伴う呼び出しをすべて拒否します。

なぜ plist を決して編集しないのですか?

意図的です。Deployboard は launchd の状態を読み、launchctl にラベルの start、stop、reload を頼めますが、ディスク上の plist は書き換えません。plist エディタが必要なら、定番の有料オプションは LaunchControl です。このツールが扱うのは、デプロイが何をしているかを知ることです。