Linux Cron Job Not Running: Resolving Environment PATH and Shell Variable Issues

Troubleshoot why your Linux cron jobs aren't executing due to missing environment PATH variables, incorrect shell context, or daemon limitations.


Introduction

Have you ever set up a cron job only to find it never runs, or fails silently, despite working perfectly when executed manually from your terminal? This is a classic symptom of environment variable discrepancies between your interactive shell session and the minimal environment provided to cron jobs by the cron daemon. The most common culprit is a missing or incomplete PATH variable, preventing cron from locating necessary commands or scripts. This guide will walk you through diagnosing and resolving these elusive environment-related cron issues.

Symptom & Error Signature

The primary symptom is that a scheduled task simply doesn’t execute or complete its function, with no obvious error message appearing in your interactive terminal. When a cron job fails, it often doesn’t produce an error code directly visible to the user. Instead, you might observe:

  • No output: The expected task (e.g., backup, log rotation, script execution) simply doesn’t happen.
  • “Command not found” or “No such file or directory” in logs: If you’ve configured cron to email output or redirect to a log file, you might see errors like these for commands that are clearly available in your interactive shell.

Example of a failed cron attempt (often redirected to a log file or email):

/bin/sh: 1: my_custom_command: not found
OR
/bin/sh: 1: php: not found

Checking cron logs for execution attempts and potential errors:

grep CRON /var/log/syslog

Typical syslog output for a successful cron entry (often provides user and command):

Jun 27 10:00:01 hostname CRON[12345]: (www-data) CMD (/usr/bin/php /var/www/html/mysite/artisan schedule:run)

Typical syslog output for a problematic cron entry (daemon might execute, but script fails internally):

Jun 27 10:00:01 hostname CRON[12345]: (www-data) CMD (some_script.sh)
Jun 27 10:00:01 hostname CRON[12345]: (CRON) info (No MTA installed, discarding output)

In the second example, CRON successfully initiated some_script.sh, but the script itself likely failed and its output (which might contain the “command not found” error) was discarded because no Mail Transfer Agent (MTA) was configured.

Root Cause Analysis

The core of this problem lies in the fundamental difference between an interactive shell session and the environment provided by the cron daemon.

  1. Minimal Environment: When cron executes a job, it does so in a highly simplified, non-interactive shell environment. This environment is intentionally sparse to ensure reproducibility and prevent unintended side effects from a user’s potentially complex shell configuration.
  2. Restricted PATH Variable: The most common environmental difference is the PATH variable. In an interactive shell, PATH is extended by sourcing files like .bashrc, .profile, .zshrc, or system-wide configurations in /etc/profile or /etc/environment. These additions allow you to run commands like php, npm, composer, docker, or custom scripts by their name without specifying their full path. Cron’s default PATH is typically very minimal, often limited to /usr/bin:/bin, and sometimes /usr/local/bin:/usr/sbin:/sbin. If your required command isn’t in these directories, cron won’t find it.
  3. No Shell Initialization Files Sourced: cron does not execute ~/.bashrc, ~/.profile, or other shell initialization files for the user. This means any environment variables, aliases, or functions defined in these files (including modifications to PATH) are unavailable to the cron job.
  4. Default Shell Differences: The default shell used by cron is usually /bin/sh. On Debian and Ubuntu systems, /bin/sh is symlinked to dash (Debian Almquist Shell), which is a lightweight, POSIX-compliant shell. Your interactive shell is likely bash or zsh. While most basic commands are compatible, subtle differences in shell behavior or reliance on bash-specific features can cause scripts to fail under dash.
  5. Missing Custom Environment Variables: Beyond PATH, any other custom environment variables your script relies on (e.g., APP_ENV, DATABASE_URL, JAVA_HOME) will also be missing unless explicitly defined for the cron job.
  6. User Context: Cron jobs run under the user context specified in the crontab (crontab -e for the current user, or /etc/cron.d/ entries explicitly state the user). This user might have a different PATH or environment compared to the user you used to test the script manually.

Step-by-Step Resolution

Here’s how to systematically troubleshoot and resolve cron job environment issues.

1. Use Absolute Paths for All Commands

The most robust and often simplest solution is to explicitly specify the full, absolute path to every command and script within your cron job.

Example:

Instead of:

* * * * * php /var/www/html/mysite/artisan schedule:run

(Which assumes php is in cron’s PATH)

Change to:

* * * * * /usr/bin/php /var/www/html/mysite/artisan schedule:run >> /var/log/mysite-cron.log 2>&1

How to find absolute paths: Use which or command -v in your interactive shell:

which php
# Output: /usr/bin/php

which composer
# Output: /usr/local/bin/composer

which my_custom_script.sh
# Output: /home/myuser/bin/my_custom_script.sh

[!IMPORTANT] Always redirect stdout and stderr to a log file (>> /path/to/logfile.log 2>&1) for cron jobs. This is crucial for debugging, as it captures any output or errors the job might produce.

2. Define PATH Directly in the Crontab

If specifying absolute paths for every command becomes cumbersome, or if your script relies on many binaries not found in cron’s default PATH, you can define the PATH variable at the top of your crontab.

  1. Find your full PATH: In your interactive shell, run:

    echo $PATH

    This will output a colon-separated list of directories.

  2. Edit your crontab:

    crontab -e
  3. Add PATH at the top: Add a PATH= line at the very beginning of the file, before any cron job entries.

    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin:/usr/games:/usr/local/games
    
    # Cron job entry
    * * * * * php /var/www/html/mysite/artisan schedule:run >> /var/log/mysite-cron.log 2>&1

    [!NOTE] While adding your full interactive PATH can resolve issues, it’s generally best practice to include only the necessary directories to keep the cron environment as minimal as possible.

3. Explicitly Set SHELL in the Crontab

If your script relies on specific features of bash (like arrays, advanced parameter expansion, or certain control structures) that might not be available in dash, you can force cron to use bash.

  1. Edit your crontab:

    crontab -e
  2. Add SHELL at the top:

    SHELL=/bin/bash
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
    
    * * * * * php /var/www/html/mysite/artisan schedule:run >> /var/log/mysite-cron.log 2>&1

4. Source Environment Files Within the Cron Job

For complex environment setups where many variables are needed, or if an application’s specific environment file needs to be loaded, you can source it directly within the cron command.

Example for a user’s ~/.profile or /etc/environment:

* * * * * /bin/bash -lc 'source ~/.profile && /usr/bin/php /var/www/html/mysite/artisan schedule:run' >> /var/log/mysite-cron.log 2>&1
  • bash -l: Starts a login shell, which often sources ~/.profile (and sometimes ~/.bash_profile).
  • bash -c: Executes the string command.
  • source ~/.profile: Explicitly sources the profile file. Adjust the path as needed (e.g., source /etc/environment for system-wide variables).
  • &&: Ensures the PHP command only runs if the source command is successful.

[!WARNING] Sourcing ~/.bashrc directly is generally discouraged for non-interactive scripts, as it can have unintended side effects and assume an interactive terminal. ~/.profile or specific environment files (e.g., /etc/environment) are usually more appropriate for setting environment variables for non-login, non-interactive shells.

5. Define Specific Environment Variables

If your script requires one or two specific environment variables (e.g., APP_ENV for a Laravel application, or DB_PASSWORD), you can define them directly in the crontab.

APP_ENV=production
DB_PASSWORD=my_secure_password
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

* * * * * /usr/bin/php /var/www/html/mysite/artisan schedule:run >> /var/log/mysite-cron.log 2>&1

Alternatively, you can pass them inline if the script is simple:

* * * * * APP_ENV=production /usr/bin/php /var/www/html/mysite/artisan schedule:run >> /var/log/mysite-cron.log 2>&1

6. Debugging with a Wrapper Script

For more complex scenarios, create a small wrapper shell script that sets up the environment, then executes your main script. This allows for more advanced debugging and modularity.

1. Create run_mysite_cron.sh:

#!/bin/bash
# run_mysite_cron.sh

# Set PATH explicitly
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"

# Set other necessary environment variables
export APP_ENV="production"
# export DB_PASSWORD="my_secure_password" # Consider other secure ways for sensitive info

# Navigate to the script's directory if necessary
cd /var/www/html/mysite/ || { echo "Failed to change directory" >&2; exit 1; }

# Execute the actual command
/usr/bin/php artisan schedule:run "$@"

# Add logging for success/failure
if [ $? -eq 0 ]; then
    echo "$(date): MySite cron job ran successfully."
else
    echo "$(date): MySite cron job FAILED!" >&2
fi

2. Make the wrapper script executable:

chmod +x /path/to/run_mysite_cron.sh

3. Update crontab to call the wrapper:

* * * * * /path/to/run_mysite_cron.sh >> /var/log/mysite-cron.log 2>&1

This method centralizes environment setup and provides a single point of failure and debugging for the cron job.

7. Verify User Context

Ensure the cron job is being run by the correct user.

  • crontab -e: Edits the crontab for your current user.
  • /etc/crontab or /etc/cron.d/*: These system-wide crontabs require specifying the user that the command should run as (e.g., www-data, root).

Example from /etc/cron.d/php on Ubuntu for FPM cleanup:

# /etc/cron.d/php
09,39 * * * * root [ -x /usr/lib/php/sessionclean ] && if [ ! -d /run/systemd/system ]; then /usr/lib/php/sessionclean; fi

Here, root is explicitly set as the user for the command. If you’re managing cron jobs for a web application, often www-data is the appropriate user.

[!WARNING] Avoid running cron jobs as root unless absolutely necessary, and only if the script is fully trusted. Use the principle of least privilege. If your application needs to write to web directories, run the cron job as the web server user (e.g., www-data on Debian/Ubuntu).

By systematically applying these steps, you should be able to diagnose and resolve environment-related issues preventing your Linux cron jobs from running successfully. Remember, robust logging is your best friend when dealing with background processes like cron.