個人開発で作った自動化スクリプトを「毎日決まった時刻に動かし続ける」のは、書くこと自体より難しい。PCを閉じれば止まり、環境を移せば旧環境が生き残って二重実行する。実際に2026年8月、筆者はMacのlaunchdから自宅サーバーのsystemd timerへ自動化を移設する過程で、生成処理の中断と記事の二重投稿事故を2度起こした。この記事では、その事故の原因と、systemd timerでどう解消したかを実務の手順として残す。

2026-08-10の移設事故:何が起きたか

YouTubeショート自動生成(launchdでMacのログイン・スリープ復帰時に発火)を、自宅サーバーのsystemd user timerへ移した当日、生成処理が途中で中断した。launchdは「PCを開いている間しか発火しない」ため、Macを閉じている日は処理が丸ごと飛んでいたが、移設作業自体のタイミングでも同じ問題が表面化した格好だった。

さらに厄介だったのは10日後の2026-08-20だ。サーバー側が9:00に正常投稿したのと同じ日に、Mac側のlaunchdが21:44に3本を追加投稿した。移設は完了していたはずなのに、旧環境が復活していた。

launchctl unloadでは止まらない理由

原因は「launchctl unloadはその場限り」という仕様だった。unloadでジョブを止めても、~/Library/LaunchAgents/に置いたplistファイル自体は残るため、再起動すると全て読み直されて復活する。実際に4件のジョブ全てが復活し、二重投稿につながった。

処理済み記録(どの動画を処理したかのログ)はマシンごとにローカルで持っていたため、ファイルロックをかけていても2台のマシンが別々に「未処理」と判断して同じ元データから重複生成した。これは同一マシン内の競合対策では防げない、環境移行に特有の落とし穴だ。

launchd無効化はplistの退避+launchctl disableの二段構え

再発防止として行ったのは次の2つを両方実施することだった。片方だけでは戻ってしまう。

  • plistファイル自体を退避する:~/Library/LaunchAgents/から別ディレクトリ(例:disabled_xxx/)へ移動し、再起動時に読み込まれる経路から外す
  • launchctl disableを併用する:unloadより永続的な無効化フラグを立てる

確認はlaunchctl list | grep <対象名>とls ~/Library/LaunchAgents/ | grep <対象名>の両方で行う。片方だけの確認では「消えたように見えて実は残っている」状態を見落とす。

systemd --user timerでPCを閉じても止まらない仕組み

移設先のsystemd user timerを選んだ理由は、launchdの「開いている間しか発火しない」という制約が無いことだ。2026-08-25付の運用ガイドでも、Persistent=trueを設定するとマシンが電源オフやスリープで実行タイミングを逃しても、次回起動直後にまとめて実行される(anacron的な動作)ことがラップトップ・デスクトップ運用で特に有用だと整理されている。実際、自宅サーバーが停電などで止まっていた時間帯の分は、起動時にまとめて実行される設定にしてある。

ユーザーtimerはログアウトで止まる — linger設定が要る

systemd user timerには見落としやすい仕様がある。デフォルトでは、ユーザーがログアウトするとタイマーごと停止する。セッションに関係なく動かし続けるには

loginctl enable-linger <ユーザー名>

でlinger(居残り)を有効化する必要がある。これを入れ忘れると「サーバーは起動しているのにジョブだけ動いていない」という分かりにくい停止の仕方をする。

異常終了で自動起動登録そのものが消えることもある

バッテリー故障などで電源が瞬断すると、ユニットファイルは無事でもtimers.target.wants/のシンボリックリンクだけが失われ、タイマーがdisabled状態に戻ることがあった。エラーは何も出ない。再起動後は

systemctl --user list-timers

で稼働中のタイマーが想定数そろっているか目視確認する習慣が要る。欠けていても通知は一切出ず、黙って止まる。復旧はsystemctl --user enable --now <名前>.timer。

複数ジョブが同時に走って破綻するのを防ぐ:flockでの排他制御

Persistent=trueで「取りこぼしを later にまとめて実行」する設計にすると、今度は停電復帰時に複数のジョブが同時発火して競合する問題が起きる。非力なマシン(例:Celeron・メモリ4GB)で複数のPythonプロセスやブラウザ自動化が並走すると処理が破綻しかねない。対策は各サービスのExecStartをflockで包み、同時に走れるのは常に1本だけにする方法だ。

ExecStart=/usr/bin/flock -w 3600 /path/to/.agent.lock /path/to/script.sh

順番待ちになるだけで取りこぼしはしない。時刻をずらして日常運用では重ならないようにしていても、停電復帰のような異常系では前提が崩れるため、平常時に問題が起きなくても排他制御は入れておく価値がある。

移行前にsystemd-runとsystemd-analyze verifyで検証する

unitファイルを本番投入する前に、次の2つで事前検証しておくと事故を減らせる。

  • systemd-run:永続的なunitファイルを作らずに、コマンドラインだけで一時的なタイマー・サービスを起動できる。本番のunitファイルを書く前に、コマンドが意図通り動くかその場で試せる
  • systemd-analyze verify <unitファイル>:unitファイルの構文エラーを事前にチェックできる。systemctl daemon-reloadを忘れて「ファイルを直したのに反映されない」という初歩的なミスとは別に、構文ミス自体を早期発見できる

なお2026年9月15日にはsystemdの安定版候補v262-rc3がリリースされており、systemd-sysupdate.service/.timerがsystemd-sysupdate-update.service/.timerへ改称(互換シンボリックリンク付き)されるなど、細かな仕様変更が継続的に入っている。定期実行の基盤として使う以上、メジャーアップデート時はリリースノートを確認する習慣もセットで持っておきたい。

launchd・systemd timer・cronの使い分け

方式

PCを閉じたら

取りこぼし分の実行

ログ確認

向いている環境

launchd

発火しない

次にPCを開いた時のみ(当日中とは限らない)

個別に設定要

Macを開いている時間が主な稼働時間の個人利用

systemd --user timer

サーバーが動いていれば発火(loginctl enable-linger必須)

Persistent=trueでまとめて実行

journalctlに統合

常時稼働の自宅サーバー・VPS

cron

マシンが起きていれば発火

基本的に無い(逃した回は実行されない)

標準ではローカルメール送信のみ。MAILTO未設定だとサイレントに失敗を見逃す

常時稼働かつ監視を別途組む前提の環境

cronは実行結果を追う仕組みが薄く、スクリプトの実行パスやファイル権限の問題でサイレントに失敗しやすいとされる。systemd timerなら失敗も含めてjournalctlに残るため、常時稼働のサーバーで自動化を運用するなら追跡のしやすさで優位がある。自宅サーバーでの継続稼働全般は、Docker API 2375番ポート点検2026年9月最新のような定期点検と合わせて運用するのが安全だ。

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

まとめ

自動化は「動かし始める」より「止めずに動かし続ける」方が事故が起きやすい。launchdはPCを閉じると発火しない、launchctl unloadは再起動で復活する、systemd user timerもlinger設定を忘れるとログアウトで止まる——どれも一度事故を経験しないと気づきにくい仕様だ。移行時は旧環境の無効化を2段構えで確実に行い、systemctl --user list-timersでの目視確認、Persistent=trueとflockの組み合わせ、systemd-run・systemd-analyze verifyでの事前検証をセットにしておくと、二重実行や取りこぼしの多くは防げる。