Bash Script Works Manually But Fails in Cron
A script that works perfectly when you run it yourself but fails (often silently) under cron almost always comes down to environment differences — cron runs jobs with a much smaller, more limited environment than your interactive shell session.
The Problem
You run ./backup.sh manually and it works exactly as expected. You add it to a crontab entry, and it either does nothing, partially runs, or fails — often without any obvious error message, since cron doesn't print output to your terminal the way manual execution does.
0 2 * * * /home/user/scripts/backup.sh
Why It Happens
Cron jobs run in a minimal, non-interactive shell environment, which differs from your normal terminal session in several important ways:
PATHis much shorter under cron — commands you rely on likenode,python3, or custom tools installed via a version manager may not be found, since their directories were only added to your interactive shell's PATH through your.bashrcor.profile, which cron doesn't load- Environment variables set in your shell config files (API keys, custom variables) simply don't exist under cron, since cron doesn't source those files
- The working directory is different — cron doesn't run your script from the directory you'd normally be in, so relative file paths inside the script can point to the wrong location
- Output (both stdout and stderr) isn't shown anywhere by default — errors happen silently unless you explicitly redirect them somewhere you can check
The Fix
First, capture output so you can actually see what's failing, instead of guessing. Redirect both stdout and stderr to a log file in your crontab entry:
0 2 * * * /home/user/scripts/backup.sh >> /home/user/logs/backup.log 2>&1
Run the job manually once (or wait for the next scheduled run) and check the log file — in most cases, the actual error becomes obvious immediately, usually a "command not found" pointing directly at the PATH issue.
Use absolute paths for every command and file reference inside the script, rather than relying on PATH or the current working directory being what you expect:
#!/bin/bash
/usr/bin/rsync -av /home/user/data/ /backup/destination/
If you genuinely need environment variables (API keys, custom PATH additions) available inside the script, explicitly set them at the top of the script itself, rather than relying on shell config files cron never loads:
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin"
export API_KEY="your-key-here"
# rest of script
For the working directory issue, explicitly cd into the expected directory at the start of the script instead of assuming it:
#!/bin/bash
cd /home/user/scripts || exit 1
Still Not Working?
If the script still fails after fixing PATH, environment variables, and working directory, check whether it depends on being run by a specific user with specific permissions — cron jobs added via crontab -e run as the user who owns that crontab, which may have different file permissions or sudo access than the user you were logged in as when testing manually. Running whoami as the first line of the script temporarily, and checking the log output, confirms exactly which user context cron is actually executing under.
It's also worth checking whether the script relies on a login shell's initialization behavior specifically. Some tools (version managers like nvm or rbenv, for example) only become available after certain shell startup files run, and cron's minimal environment skips all of that by design. If your script needs one of these tools, either source the relevant initialization file explicitly at the top of the script, or reference the tool's full installed path directly instead of relying on it being available through PATH:
#!/bin/bash
source "$HOME/.nvm/nvm.sh"
nvm use 18
Finally, if the cron job involves sending output somewhere (email, a webhook, writing to a shared log), confirm that mechanism itself works under cron's environment too — a script that completes successfully but fails to notify you of its success can look identical to a silent failure from the outside, when the actual task ran fine and only the reporting step broke.