launchd

macOS の launchd で Python スクリプトを定時実行する完全設定手順【詰まりポイントまとめ】

macOS の launchd で Python スクリプトを定時実行する完全設定手順【詰まりポイントまとめ】

この記事は約 5 分で読めます

📖 目次
  1. 📌 導入
  2. 📌 launchd と cron の違い、実際のところ
  3. 📌 .plist ファイルの基本構造
  4. 📌 詰まったポイント3つ
  5. 📌 読み込みと確認コマンド
  6. 📌 複数スクリプトを管理するときの整理
  7. 📌 ログ設計の話
  8. 📌 まとめ

導入

cron を使ってたんだけど、macOS に移行してから挙動が怪しくなった。スリープ中に実行時間を過ぎると、起動後もスルーされる。毎日朝7時に SNS 投稿スクリプトを走らせたいのに、MacBook が起きるのを待たずに「今日はもういいや」ってなる。

launchd に乗り換えたのは去年の秋ごろで、最初の .plist ファイルを書くのに丸1日溶かした。今は30本以上のスクリプトを launchd で管理してて、SNS投稿・KDP生成・ブログ記事生成まで全部自動で回ってる。その経験をそのまま書く。


launchd と cron の違い、実際のところ

cron に慣れてると、launchd は最初に面食らう。* * * * * で書けたものを、XML で書かなきゃいけない。

でも慣れると launchd の方がはるかに信頼できた。スリープ復帰後に「missed」した実行を拾ってくれるし、stdout/stderr を別ファイルに吐けるし、失敗したら自動再起動もできる。

僕のパイプラインでいうと、毎日 07:00 / 12:30 / 20:30 の3回、BSky と X と note に投稿を走らせてる。cron 時代は深夜に充電しながら放置するとMacBookがスリープして7時の投稿が飛んでた。launchd に変えてから一度もそういう事故は起きてない。


.plist ファイルの基本構造

~/Library/LaunchAgents/.plist を置くのが基本。ここに置いたものは、ログインユーザー権限で動く。

[コード略 — 詳細は元記事参照]

Label はユニークな逆ドメイン形式で書くのが慣例。ファイル名もこれと合わせる(com.taito.sns-post.plist)。


詰まったポイント3つ

python3 のパスが通らない

一番最初に踏んだ。ターミナルで which python3 すると /usr/bin/python3 が返ってくるのに、launchd から実行すると「command not found」が出た。

原因は環境変数 PATH が launchd 環境では別物だから。上の例みたいに EnvironmentVariables セクションに明示的に書く必要がある。

homebrew 経由の Python を使ってるなら /opt/homebrew/bin/python3 を指定する。絶対パスで書いたほうが確実。

[コード略 — 詳細は元記事参照]

仮想環境の pip モジュールが読み込まれない

スクリプトがローカルの仮想環境(.venv)のパッケージを使ってる場合、launchd は .venv の存在を知らないのでインポートエラーになる。

解決策は2つあって、僕は「仮想環境の Python を直接指定する」方法を使ってる。

[コード略 — 詳細は元記事参照]

もう一つは shell スクリプトをラッパーにして source .venv/bin/activate してから呼ぶ方法だけど、ログが分散するのが嫌でやめた。

作業ディレクトリが違う

スクリプト内で相対パスを使ってると、launchd から実行したときに FileNotFoundError が出る。launchd のデフォルト作業ディレクトリは / だから。

[コード略 — 詳細は元記事参照]

これを追加するか、スクリプト側で絶対パス指定に統一するか。僕は両方やってる。


読み込みと確認コマンド

.plist を書いたら launchctl で登録する。

[コード略 — 詳細は元記事参照]

launchctl list で出てくる数値の見方。

[コード略 — 詳細は元記事参照]

PID が - で終了コードが 0 なら正常に完了してる。PID に数値が出てれば今実行中。終了コードが -12 とかになってたらエラー——ログを確認しに行く。


複数スクリプトを管理するときの整理

今は30本以上あるので、命名規則を決めてる。

[コード略 — 詳細は元記事参照]

常駐型(KeepAlive)と定時型(StartCalendarInterval)を混在させても問題ない。常駐型は KeepAlivetrue にするだけ。

[コード略 — 詳細は元記事参照]

プロセスが落ちたら launchd が自動で再起動してくれる。Discord Bot はこれで動かしてて、クラッシュしても数秒以内に復活する。✨


ログ設計の話

ログを雑に設計すると、何が起きてるか追えなくなる。僕の設計は:

  • stdout04_Config/logs/{name}.log(正常出力)
  • stderr04_Config/logs/{name}_err.log(エラー)

スクリプト側でも Python の logging モジュールを使って、実行日時・処理件数・エラー内容を必ず出力してる。launchd は実行のたびにログを追記してくれるけど、スクリプト側で ---- のセパレーターを入れて区切りを明示してる。

[コード略 — 詳細は元記事参照]

まとめ

cron より設定が面倒なのは本当で、最初の1本を動かすまでが一番しんどい。でも動き出したら安定してて、スリープ復帰後の取りこぼしもなく、ログも綺麗に管理できる。

「手で実行したら動くのに launchd から動かない」という問題のほぼ全部は、環境変数かパスかディレクトリの問題。この3点を先に確認する癖をつけると解決が早い。

今は朝7時に起きると SNS への投稿が全部済んでて、KDP の原稿も生成されてる。ここまで来るのに半年かかったけど、launchd の設定でつまずいた時間が一番長かった気がする。誰かの1時間節約になれば。


[コード略 — 詳細は元記事参照]

👨‍💻
一ノ瀬泰斗
AI自動化エンジニア / Python個人開発者

Claude Code × Ollama × launchd で SNS・ブログ・KDPを全自動化。実測データと失敗談を軸に、月5万円収益化のリアルな記録を発信中。