Developer guide
Cron PATH & Environment Variables: Why Your Bash Script Runs Differently
The script runs perfectly from a terminal and fails from cron with command not found — or silently never fires. The reason is almost always the environment: cron hands your job a minimal PATH, no shell profile, and none of the variables your login shell sets. This guide shows exactly what cron strips out and how to put it back.
Written by Benjamin Rotshtein
Updated
Why does cron say "command not found" when my script works in the terminal?
Because cron runs your job with a nearly empty environment: a minimal PATH (usually /usr/bin:/bin), no .bashrc, and no aliases or exported variables. So python or docker from your shell are invisible to cron. Fix it with absolute paths in the crontab line and a shebang, or source the profile inside the script.
- Why does cron fail where bash works?
- Cron uses a minimal environment: default PATH
/usr/bin:/bin, no .bashrc, no aliases. Your tools simply are not on that PATH. - Does cron load .bashrc?
- No. The profile files load only for interactive or login shells. Source them yourself or export what you need.
- Python venv not found?
- Call the venv interpreter by absolute path:
/home/me/env/bin/python3. No activation needed.
The environment cron gives you
When cron fires a job it does not start a login shell. The child process inherits a deliberately small environment: a default PATH of roughly /usr/bin:/bin, HOME and LOGNAME, and almost nothing else. No aliases, no functions, no variables from ~/.bashrc. Verify it yourself with a throwaway job that dumps the environment:
* * * * * env > /tmp/cron-env.txt 2>&1 # then compare with your terminal's: env | sort
The difference between those two files is the entire bug. Everything your terminal has that cron does not is a variable you must now provide yourself.
Fix 1: absolute paths and a full PATH in the script
The most robust fix never depends on cron’s PATH at all. Use the absolute path to every binary, or export the PATH your script needs at the top:
#!/bin/bash export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/snap/bin:$PATH" # version-manager tools (node, python, etc.) export NVM_DIR="$HOME/.nvm" [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # absolute paths also work without any export /usr/bin/python3 /home/me/backup.py /usr/local/bin/docker compose up -d
A script that exports its own environment runs identically no matter which crontab, machine or container launches it — which is exactly why this is the pattern to standardize on.
Fix 2: source the profile yourself
If you genuinely want the interactive environment, load it at the top of the script. Cron will not do it for you:
#!/bin/bash [ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc" [ -f "$HOME/.profile" ] && . "$HOME/.profile"
Keep in mind this pulls in interactive-only code too (PS1 tweaks, aliases) that can break a non-interactive run. Prefer exporting explicit variables unless you need the full profile.
Fix 3: set variables in the crontab itself
Variable assignment lines at the top of a crontab apply to every job below them. This is where PATH and MAILTO belong:
# crontab -e MAILTO="ops@example.com" PATH="/usr/local/bin:/usr/bin:/bin" MY_API_KEY="sk-..." 0 4 * * * /opt/scripts/backup.sh * * * * * /opt/scripts/check.sh >> /var/log/check.log 2>&1
Fix 4: Python scripts and virtualenvs under cron
“Python script not running” under cron is usually one of two things. Either the interpreter is not on cron’s PATH (python3 lives in /usr/local/bin or a pyenv shim), or the script needs the venv’s packages. Both are solved by invoking the venv interpreter by absolute path — no source activate required:
#!/bin/bash # 1) venv interpreter directly — packages from the venv are used /home/me/proj/.venv/bin/python3 /home/me/proj/report.py # 2) same via crontab line # 0 6 * * * /home/me/proj/.venv/bin/python3 /home/me/proj/report.py >> /home/me/proj/report.log 2>&1
Using .venv/bin/python3 pins the script to that environment and is immune to PATH problems. The deeper debugging checklist lives in the Python + cron guide.
Fix 5: cron.d files and Docker images
System-wide /etc/cron.d fragments can carry their own environment and must name the user in the sixth field:
# /etc/cron.d/backup SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin * * * * * root /opt/scripts/backup.sh
Inside Docker there is usually no interactive profile at all, so the same rules become mandatory: declare everything with ENV in the image or an env: block, and run the job via a supervisor such as supercronic so the process stays alive and the environment is explicit.
Frequently asked questions
Why does cron say 'command not found' when the command works in my terminal?
Cron launches your command in a minimal environment with a default PATH of roughly /usr/bin:/bin plus HOME and LOGNAME. Tools installed by a package manager, a version manager, or a language runtime (node, python, docker, uv, pyenv) live outside that PATH. Your login shell added them via .bashrc or .profile — cron never reads those. Fix it by exporting the full PATH at the top of the script or using absolute paths to every binary.
Does cron read .bashrc or .bash_profile?
No. .bashrc loads for interactive shells and .bash_profile for login shells; a cron job is neither. The command runs under /bin/sh (or the script's shebang) with the empty environment. If your script depends on dotfiles, source them explicitly at the top: . "$HOME/.bashrc" — or better, export the values you need directly.
How do I set PATH and environment variables for cron jobs?
Inside a crontab, add a variable assignment line before the job: PATH=/usr/local/bin:/usr/bin:/bin and MAILTO=... apply to every following entry. For a single script, export PATH and any other variables at the top of the file itself, which is the most reliable approach because it survives moving the job to another crontab or machine.
Why is my Python script not running under cron (or failing to find its venv)?
Two common causes. First, the minimal PATH cannot find python3 or the venv's bin directory, so the script never starts — use the venv's absolute interpreter path, e.g. /home/me/myenv/bin/python3 /home/me/script.py. Second, even when it starts, the venv must be activated or invoked directly, because cron never sources your shell's activation. Calling the venv interpreter by absolute path sidesteps activation entirely.
What is cron.d and how is it different from crontab -e?
cron.d is a directory of system-wide crontab fragments (e.g. /etc/cron.d/backup). Each file is read by cron directly and must name the user in the sixth field: * * * * * root /usr/local/bin/backup.sh. Unlike the user crontab it can carry its own PATH and environment lines, which makes it handy for locking down a known environment, but on containers you must ensure cron is actually installed and running.
How do I run a cron job inside Docker with the right environment?
Define everything the job needs as environment variables in the container (env: or ENV) and invoke the script with absolute paths. Because the container image has a fixed PATH and no interactive profile at all, the cleanest pattern is ENTRYPOINT running a supervisor (like the cron process itself or supercronic) with the job's environment declared in the image — never rely on a shell profile that may not exist inside the container.
Related guides
Build a schedule that survives
Once the environment is right, generate the crontab line from plain English and preview the next fire times so a “works in my terminal” bug never wastes another day.