Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ubuntu Cron Jobs Guide

By Kokil Thapa | Last reviewed: August 2026

Scheduling automated tasks is fundamental to running production web systems, yet misconfigured schedulers cause more silent failures than almost any other infrastructure component. This Ubuntu Cron Jobs Guide provides the exact syntax, debugging workflows, and environment handling required to make scheduled tasks reliable on modern Linux servers. Whether you are maintaining a legacy PHP application or deploying a new Laravel system, understanding how the daemon actually executes your commands prevents the "it works locally but fails on the server" class of bugs that plague deployments.

For developers building applications in Nepal or managing remote infrastructure, getting this right is non-negotiable. I have seen countless projects where backups stopped silently because a path changed during an OS upgrade, or where email notifications piled up because the mail transfer agent was missing. If you are looking for broader context on server management for applications, my article on hiring a Laravel developer in Nepal covers the full stack including infrastructure expectations, but this guide focuses strictly on the scheduler itself.

How do you correctly write Ubuntu Cron Jobs syntax?

The most common point of failure in any Ubuntu Cron Jobs Guide is the assumption that the shell environment inside cron matches your interactive terminal. It does not. Cron executes commands with a minimal environment: typically only HOME, LOGNAME, SHELL=/bin/sh, and PATH=/usr/bin:/bin. Any command outside that restricted path, or any dependency on environment variables set in your .bashrc, will fail silently.

The Five-Field Time Expression

Cron uses five whitespace-separated fields to define the schedule. The format is minute, hour, day-of-month, month, and day-of-week. A frequent mistake is confusing the day-of-month and day-of-week fields; setting both to specific values (not *) creates an OR condition in standard Vixie cron, meaning the job runs if either matches. This trips up many developers who intend AND logic.

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of week (0 - 7, Sun=0 or 7)
# │ │ │ │ │
# * * * * * command-to-execute

# Run daily at 3:30 AM Nepal Time
30 3 * * * /usr/bin/php /var/www/app/artisan schedule:run >> /var/log/app-cron.log 2>&1

# Run every Monday at 9:00 AM
0 9 * * 1 /usr/local/bin/backup-script.sh

# WRONG: Runs on 1st/15th OR Mondays (not AND)
0 9 1,15 * 1 /some/command

Absolute Paths Are Mandatory

Never rely on $PATH resolution inside crontab. Always specify the full path to binaries and scripts. On Ubuntu 24.04 LTS, PHP might be at /usr/bin/php8.3 or /usr/bin/php depending on your installation method. Verify with which php or readlink -f $(which php) in your interactive shell, then hardcode that result. For Node.js applications using NVM, the binary path includes the version directory and must be resolved explicitly since NVM modifies PATH dynamically in interactive shells only.

Crontab File/etc/crontab~/.config/cronCron DaemonParses ScheduleMinimal ENVCommand Exec/usr/bin/phpAbsolute Path OnlyCommon Failure Points✗ Relative paths (php artisan)✗ Missing env vars ($DB_HOST)✗ Interactive shell assumptions✗ Unredirected stderr outputProduction Fixes✓ Full binary paths always✓ Explicit ENV in crontab✓ Output to log files✓ Test with run-parts first
Ubuntu Cron Jobs execution flow showing why absolute paths and explicit environments prevent silent failures

Environment Variable Declaration

You can declare environment variables directly at the top of your crontab file before any schedule entries. This is preferable to sourcing profile scripts, which introduces interactive shell dependencies and unpredictable side effects. Set only what the command actually needs:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
APP_ENV=production
DB_HOST=127.0.0.1

# Now commands inherit these variables
0 2 * * * /usr/bin/php /var/www/app/artisan db:backup

On production systems I maintain, I keep environment configuration in the application's .env file and ensure the cron command loads it explicitly rather than duplicating secrets in crontab. For Laravel applications, the framework loads .env automatically when you call artisan with the full path, but custom PHP scripts may need dotenv or explicit variable passing.

Why are my Ubuntu Cron Jobs not running or failing silently?

Silent failure is the default behaviour of cron. If a command exits non-zero, produces no output, and you have not configured MAILTO, you will never know it failed. Debugging requires systematic elimination of the four most common causes: path issues, permission problems, environment mismatches, and timing conflicts.

Check the Cron Logs First

Ubuntu logs cron activity to /var/log/syslog by default (or /var/log/cron.log if rsyslog is configured to separate it). Filter for cron entries to see whether the daemon attempted execution:

# View recent cron executions
grep CRON /var/log/syslog | tail -50

# Check for permission denied or missing binary errors
grep -i "cron\|error\|denied" /var/log/syslog | tail -100

# Enable verbose cron logging temporarily
# Edit /etc/rsyslog.d/50-default.conf
# Uncomment: cron.* /var/log/cron.log
sudo systemctl restart rsyslog

If syslog shows the command executed but your application did not behave as expected, the problem is inside the command itself, not the scheduler. Redirect both stdout and stderr to a log file to capture runtime errors:

# Capture all output for debugging
0 3 * * * /usr/bin/php /var/www/app/artisan sync:orders >> /var/log/sync-orders.log 2>&1

# Include timestamp for correlation
0 3 * * * echo "$(date '+\%Y-\%m-\%d \%H:\%M:\%S') Starting sync" >> /var/log/sync.log && /usr/bin/php /var/www/app/artisan sync:orders >> /var/log/sync.log 2>&1

Note the escaped percent signs in the date format string. Cron interprets unescaped % as newline characters, which breaks the date command and causes silent failures. This is one of the most frequently missed details in any Ubuntu Cron Jobs Guide.

Permission and Ownership Issues

Cron runs as the user whose crontab owns the entry. If your script writes to /var/www/app/storage/logs but the crontab belongs to root while the web server runs as www-data, you create permission conflicts that manifest as intermittent failures. Always match the cron user to the application owner. On shared hosting or multi-tenant servers, verify ownership with ls -la on target directories and ensure the cron user has write access.

File permissions on the script itself matter too. Scripts must be executable (chmod +x script.sh) if invoked directly, or you must invoke them through an interpreter explicitly (/bin/bash /path/to/script.sh). I prefer the latter approach because it removes ambiguity about shebang lines and works consistently across different deployment methods.

Testing Without Waiting

Never deploy a new cron entry and wait for the scheduled time to verify it works. Test immediately using three methods:

  1. Manual execution: Copy the exact command from crontab and run it in a clean environment: env -i /bin/bash --noprofile --norc -c 'your-command-here'. This simulates cron's minimal environment.
  2. Run-parts validation: For scripts in /etc/cron.daily or similar directories, test with run-parts --test /etc/cron.daily to verify naming conventions (no dots, no extensions) allow execution.
  3. Minute-level test: Temporarily set the schedule to * * * * * (every minute), watch the log for two successful executions, then change to the intended schedule. Never leave a per-minute job active in production.

How do you manage Laravel Scheduler with Ubuntu Cron?

Laravel's task scheduler exists specifically to avoid editing crontab for every new scheduled task. Instead of managing multiple cron entries, you add a single entry that invokes the scheduler every minute, and define all tasks in app/Console/Kernel.php (Laravel 10 and earlier) or routes/console.php (Laravel 11+). This is the pattern I use on every Laravel project I ship, from legal-tech portals like Court Marriage In Nepal to eCommerce platforms like Nepal Gift Card.

The Single Cron Entry

Add exactly one entry to your application user's crontab:

* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1

The cd is critical. Artisan commands often assume they run from the project root, and without changing directory first, relative paths in your scheduled tasks will break. The output redirect to /dev/null prevents cron from emailing you every minute; Laravel handles its own logging through the framework's log channels.

Defining Scheduled Tasks

In Laravel 12 (current stable as of 2026), scheduled tasks are defined in routes/console.php using the Schedule facade:

use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send-digest')->dailyAt('08:00');
Schedule::command('reports:generate')->weeklyOn(1, '06:00');
Schedule::call(function () {
    // Closure-based tasks for simple operations
    Cache::flush();
})->hourly();

// Prevent overlapping executions
Schedule::command('sync:inventory')->everyFiveMinutes()->withoutOverlapping();

// Run only on specific environments
Schedule::command('billing:charge')->monthly()->onOneServer();

The withoutOverlapping() method is essential for tasks that might run longer than their interval. Without it, a slow inventory sync that takes seven minutes will spawn a second instance while the first is still running, causing race conditions and duplicate processing. Laravel uses cache locks to prevent this, so ensure your cache driver (Redis recommended) is properly configured.

Raw Cron Approach• Multiple crontab entries• Schedule logic in system config• No overlap protection• Hard to version controlLaravel Scheduler• Single cron entry (* * * * *)• Schedule in routes/console.php• Built-in overlap prevention• Version controlled + testableSystem Crontab0 8 * * * /usr/bin/php ... emails0 6 * * 1 /usr/bin/php ... reports*/5 * * * * /usr/bin/php ... sync0 0 1 * * /usr/bin/php ... billingSingle Entry + App Code* * * * * php artisan schedule:run↓ Framework resolves ↓All tasks in console.phpWith overlap/env guards
Comparing raw Ubuntu Cron Jobs management versus Laravel Scheduler delegation for production applications

Server-Level Considerations

When deploying Laravel applications on Ubuntu 24.04 with PHP 8.3 or 8.4, ensure the PHP CLI binary matches your FPM version. Running php -v should show the same major version as your web server. Mismatches cause subtle bugs where scheduled tasks use different extensions or ini settings than web requests. On servers managed with Deployer 7, I symlink the release directory and ensure the cron entry points to the current symlink path, which survives zero-downtime deployments without crontab edits.

For applications requiring precise timing or distributed execution across multiple servers, consider Laravel's onOneServer() method combined with Redis or Memcached. This ensures only one server in a cluster executes the task, preventing duplicate charges or notifications. The cache driver must support atomic locks; file and database drivers do not provide reliable locking under concurrency.

What are the security and maintenance best practices for production cron?

Treating cron as infrastructure-as-code rather than ad-hoc system configuration separates reliable systems from fragile ones. Every scheduled task should be auditable, monitored, and recoverable.

Least Privilege Execution

Never run application cron jobs as root unless absolutely necessary. Create a dedicated service user or use the application owner account. Root-level cron entries should be reserved for system maintenance (log rotation, package updates, certificate renewal). Application tasks like queue workers, report generation, and data syncs should execute with the minimum permissions required. This limits blast radius if a script is compromised or contains a bug that deletes files.

Monitoring and Alerting

Cron's native email notification is insufficient for production monitoring. Implement health checks using one of these patterns:

  • Dead man's switch: Use external services like Healthchecks.io or Cronitor. Your cron job pings a unique URL on success; the service alerts you if the ping stops arriving. This catches both command failures and server-level issues like cron daemon crashes.
  • Log-based monitoring: Configure Promtail/Filebeat to ship cron logs to Loki/Elasticsearch, then alert on absence of expected log patterns. This integrates with existing observability stacks.
  • Application-level heartbeats: Write a timestamp to Redis or database on each successful execution. A separate monitoring job checks freshness and alerts via Slack/email/PagerDuty if stale.

On client projects where budget constraints rule out paid monitoring services, I implement the heartbeat pattern using Redis. A simple Redis::set('cron:last-sync', now()) at the end of each task, checked by a separate lightweight monitor, costs nothing and catches failures within minutes.

Idempotency and Recovery

Design every scheduled task to be safely re-runnable. If a payment processing job fails halfway through, running it again should not create duplicate charges. Use database transactions, idempotency keys, or status flags to track progress. When a task fails, the next execution should pick up where the previous left off rather than restarting from scratch or skipping incomplete records.

PracticeRisk If IgnoredImplementation Effort
Absolute paths everywhereSilent failures after OS/package upgradesLow — verify once per deployment
Output redirection to logsNo visibility into failuresLow — append to every cron line
Dedicated non-root userPrivilege escalation, accidental system damageMedium — initial setup only
External health monitoringUndetected failures for days/weeksMedium — integrate once per project
Idempotent task designDuplicate operations, data corruptionHigh — requires architectural planning
Version-controlled schedulesConfiguration drift, unreproducible deploysLow — use Laravel Scheduler or Ansible

Log Rotation and Disk Space

Cron jobs that append to log files without rotation will eventually fill the disk, causing cascading failures across the entire server. Configure logrotate for every application log that cron writes to:

# /etc/logrotate.d/myapp-cron
/var/log/myapp-cron.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 appuser appgroup
}

Test rotation with logrotate -d /etc/logrotate.d/myapp-cron before deploying. The -d flag performs a dry run without modifying files. On production servers I maintain, disk space exhaustion from unrotated logs is among the top three causes of unplanned downtime, entirely preventable with fifteen minutes of configuration.

How does Ubuntu Cron compare to systemd timers and other schedulers?

While cron remains the default choice for simple recurring tasks, systemd timers offer advantages for complex scenarios. Understanding when to use each prevents over-engineering simple jobs or under-engineering critical ones.

Need Scheduled Task?Is it application-specific?YESNOApp SchedulerLaravel / Symfony / NodeSystem-Level?Backups, certs, cleanupBest For:• Business logic tasks• Version-controlled schedules• Overlap/env awarenessChoose Based On:Simple → Cron (this guide)Complex deps → systemd timerDistributed → External service
Decision framework for selecting Ubuntu Cron Jobs versus systemd timers or application-level schedulers

Systemd timers provide dependency management (wait for network/database), randomized delays to prevent thundering herd on boot, monotonic timing (run X minutes after last completion rather than at fixed clock times), and integrated journal logging. They are superior for system maintenance tasks with complex requirements. However, they require two unit files per task (service + timer), lack the simplicity of a single crontab line, and are harder to manage per-user. For application-level scheduling in Laravel, Symfony, or Node.js ecosystems, application-native schedulers remain the better choice because they understand framework context, database connections, and cache locks.

Cron persists as the pragmatic default because it is universally available, requires no additional configuration for basic use, and every Linux administrator understands it. The decision matrix is straightforward: use your application's scheduler for business logic, cron for simple system tasks, and systemd timers only when you need features cron cannot provide. Over-engineering the scheduler creates maintenance burden without improving reliability.

Making Ubuntu Cron Jobs Reliable in Production

This Ubuntu Cron Jobs Guide has covered the technical mechanics, but reliability ultimately comes from treating scheduled tasks with the same rigor as application code. Use absolute paths, declare environments explicitly, redirect output to rotated logs, implement health monitoring, and test every new entry before trusting it. For Laravel applications, delegate to the framework scheduler and maintain only a single cron entry. These patterns have proven reliable across hundreds of production deployments I have managed over fifteen years.

If you are struggling with silent cron failures, need help migrating legacy scheduled tasks to a modern architecture, or want to audit your existing scheduler configuration for production readiness, reach out through my contact page. I regularly help teams in Nepal and worldwide untangle scheduling issues that have been causing intermittent problems for months. You can also explore my full-stack development services for comprehensive application and infrastructure support, or read my guide on DevOps automation in Nepal for broader infrastructure patterns beyond cron.

Frequently Asked Questions

Always use the crontab -e command instead of editing /etc/crontab or /var/spool/cron files directly. This validates syntax before saving and prevents permission errors. On Ubuntu 24.04, it defaults to nano unless VISUAL or EDITOR environment variables are set. Never edit system cron files manually as root without validation, as a single syntax error can break all scheduled tasks silently until the next daemon reload.

Five fields: minute hour day-of-month month day-of-week followed by the command. Use absolute paths for binaries since cron has a minimal PATH environment variable. Test expressions with crontab.guru before deploying to production servers to avoid scheduling mistakes that waste debugging time during maintenance windows.

Cron runs with a restricted environment lacking your shell profile, PATH variables, and working directory context. Always specify absolute paths for executables and files, source required environment variables within the script, and redirect both stdout and stderr to log files for diagnosis. In my experience deploying Laravel applications on Ubuntu servers, missing absolute paths cause most silent cron failures after deployment.

Run systemctl status cron to verify the daemon is active and enabled. Check /var/log/syslog or journalctl -u cron for execution history and errors. If the service is inactive, start it with sudo systemctl enable --now cron. On minimal Ubuntu installations, cron may not be preinstalled and requires apt install cron before any scheduled tasks will execute.

User-specific crontabs reside in /var/spool/cron/crontabs/ with filenames matching usernames. System-wide schedules live in /etc/crontab and /etc/cron.d/ directories. Never edit spool files directly; always use crontab -e for user jobs or place properly formatted files in /etc/cron.d/ for system tasks. Direct edits bypass syntax validation and risk corrupting the cron database.

Add one entry: * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1. The scheduler then manages all application tasks internally through app/Console/Kernel.php or routes/console.php in Laravel 12. This pattern avoids crontab clutter and keeps task definitions version-controlled. I use this exact setup on legal-tech portals like Court Marriage In Nepal deployed via Deployer 7.

Scripts should be owned by the executing user with 755 permissions for executables or 644 for sourced configuration files. Avoid world-writable scripts as cron refuses to execute them for security reasons. When running PHP-FPM applications, ensure the cron user matches the file ownership to prevent permission denied errors. On shared hosting or multi-user Ubuntu servers, incorrect ownership is the most common cause of silent cron failures.

Redirect both streams: /path/to/script.sh >> /var/log/myapp/cron.log 2>&1. Create the log directory beforehand with proper ownership. For Laravel applications, let the framework handle logging through its built-in channels rather than raw redirection. Rotate logs using logrotate to prevent disk exhaustion. In production environments I maintain, unrotated cron logs have consumed entire partitions within weeks on high-frequency tasks.

Yes, use sudo -u username crontab -e to edit another user's crontab, or place a file in /etc/cron.d/ with a sixth field specifying the username. System crontabs in /etc/crontab also support a user field. Running application tasks as www-data or a dedicated service account instead of root follows least-privilege principles and prevents accidental system modifications during automated execution.

Validate syntax at crontab.guru, then test manually by running the exact command from the crontab entry in a clean shell: env -i /bin/bash --noprofile --norc -c 'your-command-here'. This simulates cron's minimal environment. For complex workflows, deploy to a staging server first. I've caught path and environment issues this way that would have caused silent failures during midnight maintenance windows on client eCommerce sites.

Common causes include multiple crontab entries for the same task, overlapping schedule definitions across user and system crontabs, or failed lock mechanisms allowing concurrent runs. Implement flock or pidfile locking in long-running scripts to prevent overlap. After migrations or server rebuilds, verify no stale entries remain in /etc/cron.d/. On Deployer 7 deployments, symlink swaps sometimes leave old release paths in crontabs causing duplicate execution until manually cleaned.

Define variables at the top of the crontab file before any schedule entries, source a .env file within the script using set -a && source /path/.env && set +a, or export variables in a wrapper script. Cron does not load .bashrc or .profile. For Laravel applications, the framework reads .env automatically when artisan commands run from the project root. Hardcoding secrets in crontab is insecure and makes rotation difficult across environments.

Upgrades may reset /etc/crontab, remove packages providing dependencies, change PHP binary paths, or alter systemd service configurations. Verify cron is still installed and enabled, check binary paths with which php, review /var/log/syslog for permission or missing dependency errors, and revalidate all crontab syntax. After upgrading Ubuntu 22.04 to 24.04 on production servers, I've found PHP version symlinks changed, breaking artisan commands until paths were updated.

Restrict /var/spool/cron/crontabs/ to root:cron with 730 permissions, use allow/deny lists in /etc/cron.allow and /etc/cron.deny to control user access, audit crontab changes with auditd rules, and store application task definitions in version control rather than server-local crontabs. For team environments, manage schedules through infrastructure-as-code or deployment tools like Deployer 7 to maintain an auditable trail and prevent ad-hoc modifications that bypass review.

Systemd timers offer better logging, dependency management, and randomized delays for fleet deployments. Laravel Scheduler handles application-level tasks with database-backed mutexes. Queue workers with delayed jobs suit event-driven workflows. Anacron handles missed jobs on laptops. For simple recurring tasks on stable servers, cron remains appropriate due to simplicity and universal availability. I prefer systemd timers for infrastructure tasks but keep application scheduling within Laravel's scheduler for maintainability.

Share this article

Quick Contact Options
Choose how you want to connect me: