Your script runs perfectly when you execute it from the terminal. Every command works. Every variable is set. You've tested it a dozen times.
Then you add it to cron. It fails immediately.
command not found
undefined variable
configuration file not found
permission denied (but the permissions are correct)
What changed? The script is the same. The permissions are the same. The only difference is who's running it: you versus cron.
The problem is environment variables—or rather, the complete lack of them in cron's execution environment.
This is one of the most frustrating and non-obvious problems in cron. Your terminal session has dozens of environment variables set automatically (PATH, HOME, USER, and application-specific ones). Cron has almost none of them.
This guide explains exactly why this happens and shows you three robust solutions with clear guidance on which to use when.
The Problem: Cron's Minimal Environment
First, understand just how bare-bones cron's environment really is.
Your Interactive Shell's Environment
When you log in and open a terminal, your shell (bash, zsh, etc.) sets up a rich environment by running several configuration files:
Login sequence:
/etc/profile(system-wide)~/.bash_profileor~/.profile(user-specific)~/.bashrc(for interactive shells)- Any custom RC files
These files set:
PATH- Where to find commandsHOME- Your home directoryUSER- Your username- Application paths (
NVM_DIR,PYENV_ROOT,RBENV_ROOT) - API keys and credentials
- Database connection strings
- Custom application variables
- Aliases and functions
See your environment:
env
Example output (truncated):
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/user/.local/bin:/home/user/.nvm/versions/node/v18.0.0/bin
HOME=/home/user
USER=john
SHELL=/bin/bash
LANG=en_US.UTF-8
NVM_DIR=/home/user/.nvm
PYENV_ROOT=/home/user/.pyenv
DATABASE_URL=postgresql://localhost/myapp
API_KEY=sk_live_abc123def456
EDITOR=vim
... (50+ more variables)
You have 50-100+ environment variables set automatically.
Cron's Environment
When cron runs a job, it starts with a nearly empty environment.
See cron's environment:
# Add this temporary cron job
* * * * * env > /tmp/cron-env.txt
Wait one minute, then check:
cat /tmp/cron-env.txt
Example output:
HOME=/home/user
LOGNAME=user
PATH=/usr/bin:/bin
SHELL=/bin/sh
That's it. Four variables.
Notice what's missing:
- ❌ No extended PATH (no
/usr/local/bin, no~/.local/bin) - ❌ No language version managers (NVM_DIR, PYENV_ROOT)
- ❌ No application config (DATABASE_URL, API_KEY)
- ❌ No custom variables from
.bashrc - ❌ No aliases or functions
- ❌ Different SHELL (
/bin/shinstead of/bin/bash)
Why This Breaks Your Scripts
Example script that works in terminal:
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Uses node (found via NVM in PATH)
node --version
# Uses custom variable
echo "Deploying to: $DEPLOY_ENV"
# Uses database URL
psql "$DATABASE_URL" -c "SELECT version();"
# Uses AWS credentials
aws s3 ls s3://my-bucket/
Running in terminal:
./deploy.sh
Output:
v18.0.0
Deploying to: production
PostgreSQL 14.5
2025-01-09 file.txt
Everything works.
Running in cron:
# Crontab entry
0 2 * * * /home/user/scripts/deploy.sh >> /var/log/deploy.log 2>&1
Output in log:
./deploy.sh: line 4: node: command not found
Deploying to:
psql: error: connection to server failed: could not connect to server
aws: command not found
Everything fails.
Why?
nodeisn't in cron's minimal PATH$DEPLOY_ENVisn't set$DATABASE_URLisn't setawsisn't in cron's minimal PATH
The Root Cause
Cron intentionally runs with a minimal environment:
- Security - Prevents scripts from accessing sensitive variables accidentally
- Consistency - Same environment regardless of which user runs it
- Predictability - No hidden dependencies on user config
- Isolation - Jobs don't interfere with each other
But this means YOU must explicitly provide every variable your script needs.
Solution 1: Declare Variables in Crontab
The most straightforward solution: define variables directly in your crontab file.
Basic Syntax
Variables set at the top of your crontab apply to all jobs below them:
# In crontab -e
# Set variables
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
HOME=/home/user
SHELL=/bin/bash
DATABASE_URL=postgresql://localhost/myapp
API_KEY=sk_live_abc123def456
# Jobs use variables set above
0 2 * * * /home/user/scripts/deploy.sh
Setting PATH
PATH is the most important variable to set.
Problem: Cron's default PATH is /usr/bin:/bin. If your commands are in /usr/local/bin or custom locations, cron won't find them.
Solution: Set a complete PATH at the top of your crontab:
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
Or copy your interactive PATH:
# In terminal, get your PATH
echo $PATH
Example output:
/usr/local/bin:/usr/bin:/bin:/home/user/.local/bin:/home/user/.nvm/versions/node/v18.0.0/bin
Copy to crontab:
# In crontab -e
PATH=/usr/local/bin:/usr/bin:/bin:/home/user/.local/bin:/home/user/.nvm/versions/node/v18.0.0/bin
0 2 * * * /home/user/scripts/deploy.sh
Now cron finds commands in all those directories.
Setting SHELL
Problem: Cron defaults to /bin/sh, which is often a minimal shell (not bash).
Solution: Explicitly set bash if your scripts use bash features:
SHELL=/bin/bash
Bash features that don't work in sh:
- Arrays:
array=(one two three) - Process substitution:
<(command) - Double brackets:
[[ condition ]] - Brace expansion:
{1..10} sourcecommand (use.instead in sh)
If your scripts start with #!/bin/bash, setting SHELL=/bin/bash ensures consistency.
Setting Custom Application Variables
Problem: Your scripts rely on custom variables set in .bashrc.
Solution: Declare them in crontab:
# In crontab -e
# Application variables
DATABASE_URL=postgresql://user:pass@localhost/myapp
REDIS_URL=redis://localhost:6379
API_KEY=sk_live_abc123def456
DEPLOY_ENV=production
LOG_LEVEL=info
0 2 * * * /home/user/scripts/deploy.sh
Complete Example: Application Deployment
# File: crontab -e
# Environment setup
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin
HOME=/home/user
# Application config
NODE_ENV=production
DATABASE_URL=postgresql://app:secret@localhost/myapp
REDIS_URL=redis://localhost:6379/0
AWS_REGION=us-east-1
API_BASE_URL=https://api.example.com
LOG_DIR=/var/log/myapp
# Email notifications
MAILTO=admin@example.com
# Jobs
0 2 * * * /home/user/scripts/deploy.sh >> $LOG_DIR/deploy.log 2>&1
0 3 * * * /home/user/scripts/backup.sh >> $LOG_DIR/backup.log 2>&1
*/15 * * * * /home/user/scripts/health-check.sh >> $LOG_DIR/health.log 2>&1
Variable Scope
Variables apply to all jobs below them until redefined:
# Set default PATH
PATH=/usr/local/bin:/usr/bin:/bin
# This job uses the PATH above
0 2 * * * /home/user/job1.sh
# Override PATH for specific job
PATH=/custom/bin:/usr/local/bin:/usr/bin:/bin
0 3 * * * /home/user/job2.sh
# Reset to default PATH
PATH=/usr/local/bin:/usr/bin:/bin
0 4 * * * /home/user/job3.sh
Pros and Cons of Crontab Variables
Pros:
- ✅ Clear and explicit
- ✅ All configuration in one place
- ✅ Easy to review what variables are set
- ✅ No hidden dependencies
- ✅ Works with all cron implementations
Cons:
- ❌ Duplicates configuration (if same variables in
.bashrc) - ❌ Have to update crontab when variables change
- ❌ Sensitive data (API keys) visible in crontab
- ❌ Verbose if you have many variables
When to use:
- ✅ Small number of variables
- ✅ Job-specific configuration
- ✅ Want explicit, visible configuration
- ✅ Different values than interactive environment
Solution 2: Source a Configuration File
Instead of duplicating variables, source them from a file.
Basic Technique
Create a configuration file:
# File: /home/user/.cron_env
export PATH=/usr/local/bin:/usr/bin:/bin:/home/user/.local/bin
export DATABASE_URL=postgresql://localhost/myapp
export API_KEY=sk_live_abc123def456
export NODE_ENV=production
Source it in cron:
# In crontab -e
0 2 * * * . /home/user/.cron_env && /home/user/scripts/deploy.sh
The . /home/user/.cron_env && part:
.is the source command (same assourcein bash)- Loads all variables from the file
&&means "if sourcing succeeds, then run the script"
Sourcing .bashrc or .profile
Attempt to source your existing shell configuration:
# In crontab -e
0 2 * * * . /home/user/.bashrc && /home/user/scripts/deploy.sh
⚠️ Warning: This often doesn't work well because:
-
.bashrcmay exit early if it detects non-interactive shell:# Common in .bashrc [ -z "$PS1" ] && return # Exits if not interactive -
.bashrcmay have interactive-only commands that fail in cron:# These fail in cron alias ll='ls -la' bind '"\e[A": history-search-backward' -
Side effects -
.bashrcmight print output, breaking cron job output
Better: Create a dedicated .cron_env file with only what cron needs.
Sourcing .profile or .bash_profile
These are better candidates because they're designed for login shells:
# In crontab -e
0 2 * * * . /home/user/.profile && /home/user/scripts/deploy.sh
Or:
0 2 * * * . /home/user/.bash_profile && /home/user/scripts/deploy.sh
Still risky because:
- May contain interactive assumptions
- May source
.bashrc(inheriting its problems) - Harder to debug when things fail
Creating a Dedicated Environment File
Best practice:
# File: /home/user/.env.production
#!/bin/bash
# Environment variables for production cron jobs
# PATH
export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
# Language version managers
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # Load nvm
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
# Application config
export NODE_ENV=production
export DATABASE_URL=postgresql://app:secret@localhost/myapp
export REDIS_URL=redis://localhost:6379/0
# AWS config
export AWS_REGION=us-east-1
export AWS_PROFILE=production
# Custom app variables
export API_BASE_URL=https://api.example.com
export LOG_LEVEL=info
Make it executable (optional but good practice):
chmod +x /home/user/.env.production
Source in crontab:
# In crontab -e
SHELL=/bin/bash
0 2 * * * . /home/user/.env.production && /home/user/scripts/deploy.sh >> /var/log/deploy.log 2>&1
0 3 * * * . /home/user/.env.production && /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
Sourcing Inside the Script
Alternative: Source at the beginning of your script:
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Load environment
source /home/user/.env.production
# Now all variables are available
echo "Deploying to $NODE_ENV"
node --version
psql "$DATABASE_URL" -c "SELECT version();"
Crontab is simpler:
# In crontab -e
0 2 * * * /home/user/scripts/deploy.sh >> /var/log/deploy.log 2>&1
Pros:
- ✅ Crontab stays clean
- ✅ Environment loading is part of the script
- ✅ Easy to test (running script manually also loads environment)
Cons:
- ❌ Every script needs to source the file
- ❌ Harder to share environment across unrelated scripts
- ❌ Duplication if multiple scripts need same environment
Security Considerations for Environment Files
Problem: Environment files often contain secrets.
Solution 1: Restrict file permissions:
chmod 600 /home/user/.env.production
Only the owner can read it.
Solution 2: Separate secrets from non-secrets:
# File: /home/user/.env.production (non-sensitive)
export PATH=/usr/local/bin:/usr/bin:/bin
export NODE_ENV=production
export AWS_REGION=us-east-1
# File: /home/user/.env.secrets (sensitive, restricted permissions)
export DATABASE_URL=postgresql://app:secret@localhost/myapp
export API_KEY=sk_live_abc123def456
export AWS_SECRET_ACCESS_KEY=xyz789
Secure the secrets file:
chmod 600 /home/user/.env.secrets
chown user:user /home/user/.env.secrets
Source both:
# In crontab -e or in script
. /home/user/.env.production
. /home/user/.env.secrets
Solution 3: Use a secrets manager:
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Load non-sensitive config
source /home/user/.env.production
# Fetch secrets from vault/secrets manager
export DATABASE_URL=$(aws secretsmanager get-secret-value --secret-id db-url --query SecretString --output text)
export API_KEY=$(aws secretsmanager get-secret-value --secret-id api-key --query SecretString --output text)
# Run deployment
# ...
Pros and Cons of Sourcing Files
Pros:
- ✅ Single source of truth for environment
- ✅ Reusable across multiple cron jobs
- ✅ Easier to update (change file, not crontab)
- ✅ Can share with interactive shell (if using .profile)
- ✅ Better for complex environments (language version managers)
Cons:
- ❌ Less explicit (variables not visible in crontab)
- ❌ Harder to debug (where is variable defined?)
- ❌ File permissions must be correct
- ❌ Sourcing can fail silently
- ❌ Potential for unintended side effects
When to use:
- ✅ Many variables needed
- ✅ Same environment for multiple jobs
- ✅ Complex setup (nvm, pyenv, rbenv)
- ✅ Variables change frequently
- ✅ Want to reuse existing configuration
Solution 3: Inline Environment Variables
Set variables for a single command without affecting other jobs.
Syntax
# In crontab -e
0 2 * * * DEPLOY_ENV=production LOG_LEVEL=debug /home/user/scripts/deploy.sh
Variables before the command are set only for that command's execution.
Multiple Variables
0 2 * * * NODE_ENV=production DATABASE_URL=postgresql://localhost/myapp API_KEY=abc123 /home/user/scripts/deploy.sh
Combining with Sourcing
0 2 * * * . /home/user/.env.production && OVERRIDE_VAR=special /home/user/scripts/deploy.sh
This:
- Sources variables from
.env.production - Overrides or adds
OVERRIDE_VAR=special - Runs the script
When to Use Inline Variables
Good for:
- ✅ Job-specific overrides
- ✅ Testing different configurations
- ✅ One or two variables
Avoid for:
- ❌ Many variables (hard to read)
- ❌ Long values (makes crontab messy)
- ❌ Sensitive data (visible in process list with
ps)
Handling Language Version Managers
Special attention needed for nvm (Node.js), pyenv (Python), rbenv (Ruby), rustup (Rust).
The Problem
These tools modify your PATH and environment, but only in interactive shells:
# In .bashrc
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
Cron doesn't run .bashrc, so nvm isn't loaded, and node isn't found.
Solution 1: Use System-Installed Version
Install Node/Python/Ruby system-wide:
# Ubuntu/Debian
sudo apt install nodejs python3 ruby
# macOS
brew install node python ruby
Now commands are in standard locations:
/usr/bin/node
/usr/bin/python3
/usr/bin/ruby
Set PATH in crontab:
PATH=/usr/local/bin:/usr/bin:/bin
Cron finds them automatically.
Downside: Can't easily switch versions per project.
Solution 2: Use Absolute Path to Specific Version
Find the absolute path:
which node
# Output: /home/user/.nvm/versions/node/v18.0.0/bin/node
Use absolute path in script:
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Use specific Node.js version
/home/user/.nvm/versions/node/v18.0.0/bin/node app.js
Or in crontab PATH:
PATH=/home/user/.nvm/versions/node/v18.0.0/bin:/usr/local/bin:/usr/bin:/bin
Downside: Hardcoded version. Breaks when you upgrade.
Solution 3: Load Version Manager in Environment File
Create dedicated environment file:
# File: /home/user/.env.node
#!/bin/bash
# Load nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# Optionally set default version
nvm use default
Source in crontab:
SHELL=/bin/bash
0 2 * * * . /home/user/.env.node && /home/user/scripts/deploy.sh
Or source in script:
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Load nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# Now node is available
node --version
npm run deploy
Solution 4: Use .nvmrc for Project-Specific Version
In project directory:
# File: /home/user/projects/myapp/.nvmrc
18.0.0
In script:
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Load nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# Change to project directory
cd /home/user/projects/myapp
# Use version specified in .nvmrc
nvm use
# Now correct Node version is active
node --version
npm run deploy
Example: Python with pyenv
# File: /home/user/.env.python
#!/bin/bash
# Load pyenv
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init --path)"
eval "$(pyenv init -)"
# Set Python version
pyenv global 3.11.0
In crontab:
SHELL=/bin/bash
0 2 * * * . /home/user/.env.python && /home/user/scripts/data-processing.py
Example: Ruby with rbenv
# File: /home/user/.env.ruby
#!/bin/bash
# Load rbenv
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
In script:
#!/bin/bash
# File: /home/user/scripts/process.sh
# Load rbenv
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
# Now ruby is available
ruby --version
bundle exec rake deploy
Best Practices: Which Method to Use When
Decision Matrix
| Scenario | Best Solution | Reason | |----------|--------------|--------| | 1-3 simple variables | Declare in crontab | Clear and explicit | | Many variables (10+) | Source environment file | Avoid crontab clutter | | Same env for multiple jobs | Source environment file | DRY principle | | Job-specific override | Inline variable | Precise control | | Sensitive data | Environment file with restricted permissions | Security | | Language version manager | Source file that loads manager | Only way to load nvm/pyenv/rbenv | | Testing different configs | Inline variables | Easy to experiment | | Production stability | Declare in crontab | No external file dependencies | | PATH only | Declare in crontab | Simple, one-liner |
Recommended Patterns
Pattern 1: Simple jobs (recommended starting point)
# In crontab -e
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
Pattern 2: Complex environment (recommended for apps)
# File: /home/user/.env.myapp
export PATH=/usr/local/bin:/usr/bin:/bin
export NODE_ENV=production
export DATABASE_URL=postgresql://localhost/myapp
# ... more variables
# In crontab -e
SHELL=/bin/bash
0 2 * * * . /home/user/.env.myapp && /home/user/scripts/deploy.sh >> /var/log/deploy.log 2>&1
Pattern 3: Environment in script (recommended for self-contained scripts)
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Load environment at the top
if [ -f "$HOME/.env.myapp" ]; then
source "$HOME/.env.myapp"
else
echo "ERROR: Environment file not found"
exit 1
fi
# Script logic here
echo "Deploying to $NODE_ENV"
# ...
# In crontab -e (stays clean)
0 2 * * * /home/user/scripts/deploy.sh >> /var/log/deploy.log 2>&1
Anti-Patterns to Avoid
❌ Don't source .bashrc in crontab:
# BAD - .bashrc often fails in non-interactive context
0 2 * * * . /home/user/.bashrc && /home/user/script.sh
❌ Don't put sensitive data directly in crontab:
# BAD - API keys visible in process list
0 2 * * * API_KEY=sk_live_secret123 /home/user/script.sh
❌ Don't assume any variables exist:
#!/bin/bash
# BAD - assumes $USER is set (it might not be in cron)
backup_dir="/home/$USER/backups"
# GOOD - explicit or check if set
backup_dir="/home/john/backups"
# Or
backup_dir="${HOME}/backups"
❌ Don't source files without error checking:
# BAD - silently fails if file doesn't exist
0 2 * * * . /home/user/.env && /home/user/script.sh
# GOOD - check file exists first
0 2 * * * [ -f /home/user/.env ] && . /home/user/.env && /home/user/script.sh
Debugging Environment Issues
When your cron job fails due to environment problems:
Step 1: Capture Cron's Environment
# Add temporary job
* * * * * env > /tmp/cron-env.txt
Compare to interactive environment:
env > /tmp/interactive-env.txt
diff /tmp/interactive-env.txt /tmp/cron-env.txt
Missing variables will appear in the diff.
Step 2: Test Your Script with Cron's Environment
Run your script with minimal environment:
env -i HOME=$HOME PATH=/usr/bin:/bin /home/user/scripts/deploy.sh
env -i starts with empty environment, then you add only what cron has.
If this fails the same way as in cron, you've identified the environment issue.
Step 3: Add Debugging to Your Script
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Debug: Print environment
echo "=== Environment ===" >> /tmp/script-debug.log
env >> /tmp/script-debug.log
echo "==================" >> /tmp/script-debug.log
# Debug: Print PATH
echo "PATH: $PATH" >> /tmp/script-debug.log
# Debug: Check if commands exist
command -v node >> /tmp/script-debug.log || echo "node not found" >> /tmp/script-debug.log
command -v psql >> /tmp/script-debug.log || echo "psql not found" >> /tmp/script-debug.log
# Your actual script
# ...
Check the debug log:
cat /tmp/script-debug.log
Step 4: Verify File Sourcing Works
#!/bin/bash
# File: /home/user/scripts/deploy.sh
# Debug: Before sourcing
echo "Before source: NODE_ENV=$NODE_ENV" >> /tmp/source-debug.log
# Source environment
if [ -f "/home/user/.env.myapp" ]; then
source "/home/user/.env.myapp"
echo "Sourced .env.myapp successfully" >> /tmp/source-debug.log
else
echo "ERROR: .env.myapp not found" >> /tmp/source-debug.log
fi
# Debug: After sourcing
echo "After source: NODE_ENV=$NODE_ENV" >> /tmp/source-debug.log
env >> /tmp/source-debug.log
Step 5: Check File Permissions
# Can cron user read the environment file?
ls -l /home/user/.env.myapp
# Should show readable by owner
-rw------- 1 user user 456 Jan 09 14:23 .env.myapp
If permissions are wrong:
chmod 600 /home/user/.env.myapp
chown user:user /home/user/.env.myapp
Complete Production Example
Here's a production-ready setup combining best practices:
Environment File
# File: /home/user/.env.production
#!/bin/bash
# Production environment for cron jobs
# Permissions: chmod 600, owner only
# System PATH
export PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
# Language version manager (Node.js)
export NVM_DIR="$HOME/.nvm"
if [ -s "$NVM_DIR/nvm.sh" ]; then
\. "$NVM_DIR/nvm.sh"
nvm use default --silent
fi
# Application environment
export NODE_ENV=production
export APP_NAME=myapp
export APP_VERSION=2.1.0
# Logging
export LOG_DIR=/var/log/myapp
export LOG_LEVEL=info
# Connections (sourced from secrets file)
if [ -f "$HOME/.env.secrets" ]; then
source "$HOME/.env.secrets"
fi
# Derived variables
export APP_ROOT=/home/user/apps/$APP_NAME
export BACKUP_DIR=/var/backups/$APP_NAME
Secrets File
# File: /home/user/.env.secrets
# Permissions: chmod 600, owner only
# NEVER commit this to version control
export DATABASE_URL=postgresql://app:SecurePass123@localhost/myapp
export REDIS_URL=redis://:SecurePass456@localhost:6379/0
export API_KEY=sk_live_abc123def456ghi789
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Application Script
#!/bin/bash
# File: /home/user/scripts/deploy.sh
set -euo pipefail # Exit on errors
# Load environment
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="$HOME/.env.production"
if [ ! -f "$ENV_FILE" ]; then
echo "ERROR: Environment file not found: $ENV_FILE" >&2
exit 1
fi
source "$ENV_FILE"
# Logging function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_DIR/deploy.log"
}
# Main deployment logic
log "Starting deployment for $APP_NAME v$APP_VERSION"
log "Environment: $NODE_ENV"
# Verify required variables
for var in DATABASE_URL REDIS_URL API_KEY; do
if [ -z "${!var}" ]; then
log "ERROR: Required variable $var is not set"
exit 1
fi
done
log "All required variables are set"
# Change to app directory
cd "$APP_ROOT" || exit 1
# Run deployment
log "Running database migrations..."
npx sequelize-cli db:migrate
log "Building application..."
npm run build
log "Restarting application..."
pm2 restart myapp
log "Deployment completed successfully"
Crontab Configuration
# File: crontab -e
# Global settings
SHELL=/bin/bash
MAILTO=admin@example.com
# Deploy every day at 2 AM
0 2 * * * /home/user/scripts/deploy.sh 2>&1 | tee -a /var/log/cron-deploy.log
# Backup every day at 3 AM
0 3 * * * . /home/user/.env.production && /home/user/scripts/backup.sh 2>&1 | tee -a /var/log/cron-backup.log
# Health check every 15 minutes
*/15 * * * * . /home/user/.env.production && /home/user/scripts/health-check.sh 2>&1 | tee -a /var/log/cron-health.log
Security Checklist
# Set correct permissions
chmod 700 /home/user/scripts/
chmod 700 /home/user/scripts/*.sh
chmod 600 /home/user/.env.production
chmod 600 /home/user/.env.secrets
# Verify ownership
chown -R user:user /home/user/scripts/
chown user:user /home/user/.env.production
chown user:user /home/user/.env.secrets
# Never commit secrets to git
echo ".env.secrets" >> .gitignore
Quick Reference
Common Environment Variables for Cron
| Variable | Purpose | Example Value |
|----------|---------|---------------|
| PATH | Where to find commands | /usr/local/bin:/usr/bin:/bin |
| HOME | User's home directory | /home/user |
| SHELL | Which shell to use | /bin/bash |
| USER | Username | john |
| LOGNAME | Login name | john |
| LANG | Language/encoding | en_US.UTF-8 |
| TZ | Timezone | America/New_York |
| MAILTO | Where to send cron output | admin@example.com |
Sourcing Syntax
| Command | Meaning |
|---------|---------|
| . /path/to/file | Source file (POSIX, works everywhere) |
| source /path/to/file | Source file (bash-specific) |
| . file && command | Source file, then run command if successful |
| [ -f file ] && . file | Source file only if it exists |
Conclusion
The core problem: Cron runs with a minimal environment. Your scripts expect a rich environment.
The solutions:
- Declare in crontab - Best for simple cases, small number of variables
- Source environment file - Best for complex environments, many variables, version managers
- Inline variables - Best for job-specific overrides
Best practices:
- ✅ Always set PATH explicitly
- ✅ Use SHELL=/bin/bash if scripts need bash features
- ✅ Create dedicated environment files for cron (don't rely on .bashrc)
- ✅ Secure sensitive data with proper file permissions (chmod 600)
- ✅ Test scripts with cron's minimal environment
- ✅ Add debugging output to troubleshoot
- ✅ Document which environment variables are required
Your checklist:
- ✅ Identify which variables your script needs
- ✅ Choose appropriate solution (crontab vs. file vs. inline)
- ✅ Set PATH to include all necessary directories
- ✅ Test script with minimal environment (
env -i) - ✅ Verify file permissions (600 for environment files)
- ✅ Add logging to confirm variables are set correctly
- ✅ Document environment requirements
From broken:
# Fails: command not found, undefined variables
0 2 * * * /home/user/scripts/deploy.sh
To working:
# Success: complete environment loaded
0 2 * * * . /home/user/.env.production && /home/user/scripts/deploy.sh >> /var/log/deploy.log 2>&1
Stop debugging "command not found" at 2 AM. Set up your environment correctly once, and your cron jobs will run reliably forever.
Ready to build bulletproof cron jobs? Use our Cron Expression Generator to create schedules that work the first time.
Related Articles
Essential Cron Troubleshooting:
- Why Your Cron Job Isn't Running - Complete debugging guide
- Fixing Permission Denied Errors in Cron Jobs - Solve permission issues
- Where Did My Cron Job Output Go? - Master logging
Master Cron Fundamentals:
- The Ultimate Guide to Cron Jobs - Beginner to advanced tutorial
- Cron Operators Explained: *, /, -, and , - Understand syntax
- Organize Your Crontab Like a Pro - Best practices
Language-Specific Guides:
- Scheduling Python Scripts with Cron - Python & Django
- Node.js Cron Jobs: System vs node-cron - Node.js automation
- Mastering Cron Jobs in PHP & Laravel - PHP frameworks
Keywords: cron environment variables, cron path variable, cron .bashrc, crontab environment, cron command not found, cron source file, nvm cron, pyenv cron, cron env, environment variables crontab