The most common reason a cron job “works manually but not in cron” is the environment. When you run a script in your terminal, it inherits your login shell’s variables: PATH, language settings, variables exported in .bashrc, nvm and pyenv setups. Cron starts jobs with an almost empty environment. Here’s how to see what cron gives you, and how to set what your jobs need.

What cron provides

Typically only:

HOME=/home/alice
LOGNAME=alice
PATH=/usr/bin:/bin
SHELL=/bin/sh

No /usr/local/bin, no ~/.local/bin, no nvm or pyenv shims, no LANG, and nothing from .bashrc or .profile.

See the real cron environment

Add a temporary job:

* * * * * env > /tmp/cron-env.txt

Wait a minute, then compare with your shell:

diff <(sort /tmp/cron-env.txt) <(env | sort)

You can even reproduce cron’s environment for testing:

env -i $(cat /tmp/cron-env.txt | xargs) /bin/sh -c '/usr/local/bin/myjob.sh'

Set variables at the top of the crontab

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
[email protected]
CRON_TZ=UTC

*/10 * * * * /opt/app/bin/sync
  • SHELL: cron uses /bin/sh by default. Set bash if your command lines use bash syntax.
  • PATH: include the directories your commands live in.
  • MAILTO: where output is emailed (MAILTO="" disables mail). Needs a working mail setup. Logging to a file is usually more practical.
  • CRON_TZ: time zone for the schedules (cronie, and recent Debian cron versions).

Variable assignments in crontab are literal: PATH=$PATH:/opt/bin is not expanded in most cron implementations. Write the full value.

Load application settings (.env files)

Many apps keep configuration in a .env file. Load it inside the script (not in the crontab):

#!/bin/bash
set -euo pipefail
set -a                       # export everything that gets defined
source /opt/app/.env
set +a
cd /opt/app
exec /opt/app/venv/bin/python manage.py cleanup

Keep .env readable only by the job’s user (chmod 600). See secrets management.

Language runtimes: nvm, pyenv, virtualenvs, rbenv

Shims set up in .bashrc don’t exist in cron. Point to the exact binary:

# Python virtualenv
0 * * * * /opt/app/venv/bin/python /opt/app/task.py

# Node via nvm (use the full path to the installed version)
0 * * * * /home/alice/.nvm/versions/node/v22.11.0/bin/node /opt/app/job.js

# PHP (Laravel scheduler)
* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1

See Python virtual environments and Laravel scheduler cron.

Locale and encoding

Scripts that handle non-ASCII text may behave differently without LANG. Set it in the crontab or the script:

LANG=C.UTF-8

See locale settings.

Alternative: systemd timers

systemd timers run jobs as services, with an explicit environment (Environment=, EnvironmentFile=), logging to the journal, and dependency handling. That avoids many cron environment surprises. See systemd timers.

[Service]
Type=oneshot
EnvironmentFile=/opt/app/.env
ExecStart=/opt/app/venv/bin/python /opt/app/task.py

A safe wrapper pattern

For anything non-trivial, keep the crontab line short, and put the environment setup in a wrapper script: set PATH, load the .env file, change to the app directory, write a timestamped log line, then run the job with exec. The wrapper can be tested by hand with env -i (as above), which reproduces cron’s environment, so “works in my shell” and “works in cron” become the same thing.

Quick reference

Problem Fix
Command not found Absolute path, or PATH= in the crontab
Bash syntax errors SHELL=/bin/bash
App config missing set -a; source .env; set +a in the script
nvm/pyenv/venv Use the full binary path
Wrong time zone CRON_TZ= or adjust the schedule
See cron’s env * * * * * env > /tmp/cron-env.txt

Related: cron job not running and environment variables on Linux.

  • Linux
  • Cron
  • Bash