環境変数と PATH の注意点
cron で動かしたスクリプトが「手で打つと動くのに失敗する」最大の原因は環境変数の違いです。cron はログインシェルを通さないため PATH が最小限で、.bashrc も読み込まれません。対策として、コマンドは絶対パスで書く、スクリプト内で必要な環境変数を明示的に設定する、crontab の先頭で PATH= を定義する、のいずれかを取ります。systemd の service でも同様で、Environment= や EnvironmentFile= で明示します。
コマンド自体は正しい。ターミナルで手打ちすると問題なく動く。なのに cron に登録すると失敗する。自動化でいちばん多く、そしていちばん厄介なのがこの現象だ。
原因が見えにくいのは、コマンドを疑ってしまうからだ。結論から言うと、その大半は環境変数の違い、とりわけ PATH の違いが原因である。
なぜそうなるのかという理屈を一度きちんと理解しておけば、この症状に出くわしても落ち着いて切り分けられるようになる。
逆に理屈を知らないままだと、コマンドを疑って何度も書き直す、という的外れな試行錯誤に陥りがちだ。
🔀 同じコマンドなのに、なぜ cron だと環境が違うのか
あなたがターミナルでコマンドを打つとき、シェルは起動時に /etc/profile や ~/.bashrc、~/.bash_profile といった設定ファイルを読み込む。
この過程で PATH に各種ディレクトリが追加され、別名(alias)や独自の環境変数も設定される。つまり日頃の対話シェルは、これらの設定をすべて読み込んだ「お膳立てされた状態」なのだ。
ところが cron がジョブを起動するときは、ログインシェルを通さず、それらの設定ファイルを一切読み込まない。
その結果、cron 実行時の PATH は /usr/bin:/bin といったごく最小限のものだけになる。~/.bashrc で足したパスや、そこで定義した環境変数・alias はすべて存在しない状態でコマンドが走る。
さらに、起動の起点となるカレントディレクトリも対話時とは異なり、ホームディレクトリなど決まった場所になる。
「手では動くのに cron で動かない」のは、コマンドが間違っているのではない。この貧弱で違う環境のせいなのだ。同じ理由で、at や systemd の service でも環境は最小限になる。
🚫 その違いは、どんな失敗となって現れるのか
PATH が最小限だと、/usr/local/bin などにインストールされたコマンドが「command not found」となって失敗する。自分で入れたツールや言語ランタイムに多い。
たとえば普段 node や python3、docker、自作スクリプトを名前だけで呼んでいるとしよう。cron 環境ではそのパスが PATH に無いため見つからない。
また、~/.bashrc で export していた API キーや言語の設定(LANG など)も読まれない。日本語が文字化けする、設定値が見つからないとエラーになる、相対パスで参照していたファイルが見つからない、といった形で表面化する。
ここで重要なのは、こうした冪等な定期処理を安定して回すには、実行環境を実行のたびに同じ・既知の状態にそろえることが前提になる、という発想だ。
環境がそのつど変わると、冪等であるはずの処理も「動いたり動かなかったり」とぶれてしまう。自動化の信頼性が根本から崩れてしまう。
🧭 対策その1 — 絶対パスと、crontab での PATH 定義
もっとも確実な対策は、コマンドを絶対パスで書くことだ。node ではなく /usr/local/bin/node、自作スクリプトなら /usr/local/bin/backup.sh と、フルパスで指定すれば PATH に依存しなくなる。
どのコマンドがどこにあるかは、対話シェルで which node や command -v node と打てば絶対パスが分かる。それをそのまま crontab やスクリプトに書き写す。
使うコマンドが多い場合は、crontab ファイルの先頭で PATH をまとめて定義する方法も有効だ。crontab の冒頭に PATH=/usr/local/bin:/usr/bin:/bin と1行書いておくと、それ以降のジョブはその PATH のもとで実行される。
あわせて SHELL=/bin/bash(ジョブを動かすシェルの指定)や MAILTO=you@example.com(実行結果メールの宛先)も、同様に crontab 冒頭で指定できる設定だ。
これらの宣言は5フィールドの予定行より前に置く必要がある点に注意してほしい。
🧰 対策その2 — スクリプト自身に、環境を整えさせる
もう1つの王道は、呼び出されるスクリプトの側で、自分が必要とする環境を明示的に作ることだ。
スクリプト冒頭で export PATH=/usr/local/bin:$PATH のように PATH を補い、必要な環境変数も export DATA_DIR=/srv/data のように自分で設定する。
秘密の値(APIキーやパスワードなど)は、スクリプトに直書きすると漏えいの危険がある。権限を絞った別ファイルに置いて source /etc/myapp/env のように読み込むと、安全かつ見通しよく管理できる。
こうしておけば、cron から呼ばれても、手で実行しても、同じ環境で動くようになり、再現性が高まる。
「実行環境を呼び出し元任せにせず、スクリプトが自分で完結して用意する」のが堅実な設計だ。どこから・どんな状況で起動されても結果がぶれなくなる。
🔍 systemd でも同じか、そしてつまずいたら何を見るか
同じ問題は systemd の service ユニットにもある。service も最小限の環境で実行されるため、必要な環境変数は明示する必要がある。
service ユニット内では Environment=\"PATH=/usr/local/bin:/usr/bin:/bin\" のように個別に書くか、別ファイルにまとめて EnvironmentFile=/etc/myapp/env で読み込む。
考え方は cron とまったく同じだ。「実行時の環境はデフォルトでは貧弱なので、必要なものは自分で与える」という一点に尽きる。
最後に、つまずいたら、まずこれ。動かないときは推測でコマンドを直す前に、ジョブの中で env コマンドを実行し、その出力を env >> /tmp/cronenv.log のようにログへ吐かせてみてほしい。
すると cron や service の実行時に、実際どんな PATH・環境変数が見えているかを直接確認できる。手で打ったときの env の出力と見比べれば、何が足りないのかが一目で分かる。ほとんどの「手では動くのに自動だと動かない」問題はここで決着する。