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/shby 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.