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: September 2026

A misconfigured cron job in Ubuntu fails quietly more often than it fails loudly. Backups stop. Invoices never send. Queue workers stall. On production VPS boxes I maintain for Laravel applications in Nepal, the scheduler is usually the first place I look when something worked yesterday but not today. This guide covers syntax, listing jobs, reading cron logs, debugging silent failures, and wiring Laravel Scheduler on Ubuntu 22.04 and 24.04 LTS.

Ubuntu ships the Vixie cron implementation through the cron package. The daemon reads crontab files, wakes at the right minute, and runs commands under a stripped-down shell environment. That minimal environment is why your interactive terminal test passes while the cron job in Ubuntu fails on the server. Treat every scheduled task like deployable code: version it, test it, and monitor it.

How do you set up a cron job in Ubuntu step by step?

Setting up a cron job in Ubuntu starts with choosing the right crontab owner. Application tasks should run as the app user — www-data, deploy, or a dedicated service account — not as root. Root cron belongs to system maintenance: log rotation, certificate renewal, and package cleanup.

Install and verify the cron service

On a fresh Ubuntu 24.04 server, cron is usually pre-installed. Confirm the service is active before adding entries:

sudo systemctl status cron
sudo systemctl enable cron
sudo systemctl start cron

If status shows inactive, no schedule will fire regardless of how perfect your crontab looks. I have traced multi-day outages to a stopped cron unit after a failed package upgrade on a client VPS costing Rs 2,500/month (~USD 19).

Edit the correct crontab

Use crontab -e for the current user's jobs. Use sudo crontab -u www-data -e when the web application owner differs from your SSH login. System-wide entries live in /etc/crontab, /etc/cron.d/, and the /etc/cron.{hourly,daily,weekly,monthly} directories.

  1. Open the crontab: crontab -e
  2. Declare environment variables at the top (PATH, SHELL, TZ)
  3. Add one schedule line with absolute binary paths
  4. Redirect stdout and stderr to a dedicated log file
  5. Save, then confirm with crontab -l
  6. Watch syslog within the next minute if you used a test schedule

For Nepal-hosted servers, set timezone explicitly. Do not assume the server clock matches Kathmandu business hours:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
TZ=Asia/Kathmandu
MAILTO=""

# Daily database backup at 2:30 AM NPT (Nepal Time)
30 2 * * * /usr/bin/mysqldump -u backup_user -p'SECRET' myapp >> /var/backups/myapp.sql 2>> /var/log/myapp-cron.log

Empty MAILTO stops cron from trying to email root on every run. Email delivery rarely works on lean VPS setups anyway. Log files are the reliable audit trail.

CrontabUser or /etc/cron.dCron DaemonReads scheduleMinimal ShellNo .bashrcCommandAbsolute pathCommon Setup MistakesRelative paths like php artisanMissing TZ for NPT schedulesWrong crontab userNo log redirectionUnescaped % in date stringsProduction ChecklistFull binary paths verifiedTZ=Asia/Kathmandu setApp user owns the jobOutput logged and rotatedTested with env -i first
How a cron job in Ubuntu flows from crontab entry through the daemon to command execution with minimal environment

What is the correct cron job syntax in Ubuntu?

Cron syntax on Ubuntu follows the standard five-field format documented in the crontab(5) man page. Minute, hour, day-of-month, month, and day-of-week define when the command runs. The command itself must use absolute paths because cron's default PATH is often just /usr/bin:/bin.

The five-field schedule

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

# Every day at 3:30 AM server local time
30 3 * * * /usr/bin/php /var/www/app/artisan schedule:run >> /var/log/app-cron.log 2>&1

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

# WRONG: runs on 1st/15th OR Mondays — not both
0 9 1,15 * 1 /some/command

When both day-of-month and day-of-week are restricted, standard Vixie cron treats them as OR logic. Developers expecting AND logic get surprised jobs firing on unintended days. Test ambiguous schedules with a regex tester mindset — map each field explicitly before deploying.

Special strings and shortcuts

Some crontab implementations accept shortcuts like @daily, @hourly, and @reboot. Ubuntu's cron supports these, but I avoid them in production crontabs. Explicit five-field syntax is clearer during audits and survives team handoffs better.

Percent sign escaping

Cron treats unescaped % as a newline character. Always escape percent signs in date format strings:

# Correct — escaped percent signs
0 3 * * * echo "$(date '+\%Y-\%m-\%d \%H:\%M:\%S') sync start" >> /var/log/sync.log 2>&1

This single gotcha causes more head-scratching than almost any other cron syntax issue. I have seen it break nightly report jobs on legal-tech portals where the date stamp never appeared in logs.

How do you list cron jobs in Ubuntu?

Listing cron jobs in Ubuntu requires checking every location cron reads from. A job missing from crontab -l may still exist in a system directory or another user's crontab. The query "list cron jobs ubuntu" usually means someone scheduled a task and cannot find it — or wants to audit what is actually running.

List user crontabs

# Current user's jobs
crontab -l

# Another user's jobs (requires sudo)
sudo crontab -u www-data -l
sudo crontab -u root -l

# List all user crontabs on the system
sudo ls -la /var/spool/cron/crontabs/

List system-wide cron entries

# Main system crontab (includes user column)
cat /etc/crontab

# Drop-in configs — common on packages like certbot
ls -la /etc/cron.d/
cat /etc/cron.d/*

# Periodic script directories
ls /etc/cron.hourly/
ls /etc/cron.daily/
ls /etc/cron.weekly/
ls /etc/cron.monthly/

Package managers often install cron snippets in /etc/cron.d/ during apt installs. Certbot renewal, logwatch, and unattended-upgrades all land there. When auditing a server during a Linux administration engagement, I grep every location before declaring the schedule clean.

# One-command audit across common locations
grep -rH . /etc/cron.d/ /etc/crontab /var/spool/cron/crontabs/ 2>/dev/null

Why is my cron job in Ubuntu not running or failing silently?

Silent failure is cron's default behaviour. If a command exits with a non-zero status and produces no output, you hear nothing unless you configured logging or MAILTO. Debugging a cron job in Ubuntu means checking four areas: service status, syslog evidence, permissions, and environment mismatch.

Check cron logs in Ubuntu first

Ubuntu writes cron activity to syslog by default. The search query "cron logs ubuntu" maps directly to these commands:

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

# Filter by specific user
grep "CRON\[www-data\]" /var/log/syslog | tail -30

# Errors and permission denials
grep -iE "cron|denied|error" /var/log/syslog | tail -100

To isolate cron into its own file, uncomment the cron line in /etc/rsyslog.d/50-default.conf and restart rsyslog. Then read /var/log/cron.log directly. The Ubuntu Cron community guide documents this rsyslog split if you need persistent separation.

When syslog shows the command ran but the application did nothing, the bug lives inside the script — not the scheduler. Redirect all output during initial deployment:

0 3 * * * /usr/bin/php /var/www/app/artisan sync:orders >> /var/log/sync-orders.log 2>&1

Permission and ownership problems

Cron runs as the crontab owner. A job owned by root that writes to /var/www/app/storage owned by www-data creates intermittent permission errors. Match the cron user to the application owner. Scripts invoked directly need the executable bit, or call them through an interpreter:

# Preferred — explicit interpreter, no chmod dependency
0 4 * * * /bin/bash /var/www/app/scripts/nightly-cleanup.sh >> /var/log/cleanup.log 2>&1

Test without waiting for the schedule

  1. Simulate cron's environment: env -i /bin/bash --noprofile --norc -c '/usr/bin/php /var/www/app/artisan inspire'
  2. Validate run-parts scripts: run-parts --test /etc/cron.daily
  3. Minute-level smoke test: set schedule to * * * * *, confirm two successful log entries, then revert immediately

Never leave a per-minute production job running. On a busy Laravel app, that mistake can hammer the database within an hour.

Cron Job Not Working?Is cron service active?Fix Servicesystemctl start cronCheck Sysloggrep CRON syslogEntry Found?Command ranCommand ran but failed?Path / PermissionFix crontab entryRead App LogBug in script
Step-by-step debugging flow when a cron job in Ubuntu fails silently on production servers

How do you run Laravel Scheduler with a cron job in Ubuntu?

Laravel applications should not scatter business schedules across system crontab. One cron job in Ubuntu calls schedule:run every minute. Laravel decides which tasks fire based on definitions in application code. I use this pattern on every Laravel project — from Nepal Gift Card to booking systems like Adventure Third Pole Trek.

The single crontab entry

* * * * * cd /var/www/app/current && /usr/bin/php artisan schedule:run >> /var/log/laravel-scheduler.log 2>&1

The cd matters. Artisan assumes project-root relative paths. On Deployer 7 deployments, point cron at the current symlink so zero-downtime releases do not require crontab edits. See my guide on Deployer rollback and release paths for the full symlink workflow.

Define tasks in Laravel 12

Laravel 12 (PHP 8.3+) defines schedules in routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:daily')->dailyAt('06:00');
Schedule::command('sync:inventory')->everyFiveMinutes()->withoutOverlapping();
Schedule::command('billing:charge')->monthly()->onOneServer();

withoutOverlapping() prevents a slow sync from spawning duplicate workers. It requires a cache driver that supports atomic locks — Redis 8.10 is my default on production VPS setups. Read the comparison in my article on cron versus queue workers in Laravel when tasks exceed one-minute intervals or need retry logic.

For deeper Laravel scheduling patterns — overlap guards, onOneServer(), and monitoring — see the dedicated Laravel scheduled tasks production setup guide and the framework docs at laravel.com/docs/12.x/scheduling.

Many Cron EntriesSeparate line per taskLogic outside gitNo overlap protectionHard to test locallyLaravel SchedulerOne cron job in UbuntuTasks in routes/console.phpOverlap and env guardsVersion controlledSystem Crontab0 8 * * * php ... emails0 6 * * 1 php ... reports*/5 * * * * php ... sync0 0 1 * * php ... billingSingle Entry + App Code* * * * * schedule:runFramework resolves tasksDeployer symlink safe
Raw ubuntu cron jobs versus Laravel Scheduler — one cron job in Ubuntu replaces many system entries

PHP version alignment

Ensure the PHP CLI binary matches your PHP-FPM version. On Ubuntu with PHP 8.3 or 8.4 side by side, verify with readlink -f $(which php) and hardcode that path. Mismatched versions cause scheduled tasks to miss extensions that web requests use. My Ubuntu server setup for PHP apps covers multi-version PHP management on the same box.

What are production security and maintenance practices for Ubuntu cron?

Reliable cron on production servers is an ops discipline, not a one-time config edit. Every scheduled task needs least-privilege execution, log rotation, external health checks, and idempotent design.

Least privilege and secrets

Never run application cron as root. Reserve root crontab for system tasks like rsync backup automation and certificate renewal. Keep secrets out of crontab when possible — Laravel loads .env automatically through Artisan. Plaintext passwords in crontab show up in process lists and backup dumps.

Monitoring beyond MAILTO

  • Dead man's switch: ping Healthchecks.io or Cronitor on success; alert when pings stop
  • Log shipping: forward cron logs to Loki or Elasticsearch via Promtail or Filebeat
  • Application heartbeat: write a Redis timestamp after each run; a separate monitor checks freshness

On budget-conscious Nepal client projects, the Redis heartbeat pattern costs nothing beyond existing infrastructure. A stale key triggers a Slack webhook within minutes.

Log rotation prevents disk exhaustion

Cron jobs that append to logs without rotation will fill the disk. That causes cascading failures — MySQL crashes, PHP sessions break, the site goes down. Configure logrotate for every cron log:

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

Test with logrotate -d /etc/logrotate.d/myapp-cron. Pair this with broader Ubuntu server backup strategies so log and data retention policies align.

PracticeRisk If IgnoredEffort
Absolute paths in every cron job in UbuntuSilent failure after package upgrade changes PATHLow
Explicit TZ=Asia/Kathmandu on Nepal serversTasks fire at wrong local business hoursLow
Output redirected to rotated logsNo failure visibility; disk fills upLow
Non-root application userPrivilege escalation if script is compromisedMedium
External health monitoringOutages undetected for daysMedium
Idempotent task designDuplicate charges or double emails on retryHigh
Laravel Scheduler over raw cron sprawlConfig drift between servers and environmentsLow

How does a cron job in Ubuntu compare to systemd timers?

Cron remains the default for simple recurring tasks on Ubuntu. Systemd timers offer dependency chains, randomized boot delays, and journald integration. Choosing the wrong tool creates maintenance overhead without improving reliability.

Scheduled Task NeededApplication or system task?AppSystemLaravel SchedulerOne cron job in UbuntuSystem TaskBackup, cert, cleanupSimple schedule?Yes → cron (this guide)Deps needed → systemdBest for:Business logic in gitOverlap and env aware
When to use a cron job in Ubuntu versus systemd timers or Laravel Scheduler on production servers

Systemd timers excel when a task must wait for network availability or run relative to the last completion time. They need separate .service and .timer unit files per task. That overhead is justified for complex boot-time sequences. For everything else — nightly backups, Laravel scheduling, log cleanup — a cron job in Ubuntu is simpler and universally understood. Read my systemd services guide when timers genuinely fit better than cron.

On hardened production VPS boxes, combine cron with the practices in my Ubuntu security hardening guide and restrict who can edit crontabs through sudo policies. For full server provisioning context, see Laravel deployment on Ubuntu with Nginx and the broader DevOps automation patterns for Nepal teams.

Key Takeaways

  • Every cron job in Ubuntu needs absolute binary paths, explicit PATH, and TZ set for Nepal servers using Asia/Kathmandu.
  • List cron jobs in Ubuntu by checking user crontabs, /etc/cron.d/, and /etc/crontab — not just crontab -l.
  • Read cron logs in Ubuntu via grep CRON /var/log/syslog before blaming the application code.
  • Redirect stdout and stderr to rotated log files; never rely on cron email on headless VPS setups.
  • Laravel apps need one minute-level cron entry calling schedule:run; define all tasks in routes/console.php.
  • Test with env -i to simulate cron's minimal shell before waiting for the scheduled minute.

People Also Ask

Where is the crontab file stored in Ubuntu?

User crontabs live in /var/spool/cron/crontabs/ as files named after each user. System-wide schedules sit in /etc/crontab and /etc/cron.d/. Edit them with crontab -e, not by opening spool files directly — direct edits get overwritten.

How often does cron check for jobs in Ubuntu?

The cron daemon wakes once per minute and evaluates all crontab entries against the current time. Sub-minute scheduling is not native to cron. Use a loop, systemd timer, or application-level scheduler if you need finer granularity.

Can I run a cron job as a different user in Ubuntu?

Yes. Root can assign jobs to any user with sudo crontab -u username -e. The /etc/crontab file includes a user field in its six-column format. Always match the cron user to the application file owner.

What is the difference between cron and crontab in Ubuntu?

Cron is the background daemon that executes scheduled commands. Crontab is both the file format and the command-line tool for editing a user's schedule. You write entries with crontab -e; cron reads and runs them.

Make Every Cron Job in Ubuntu Production-Ready

A reliable cron job in Ubuntu comes down to six habits: absolute paths, explicit environment, timezone awareness for NPT servers, log redirection, systematic listing and log review, and testing before you trust the schedule. For Laravel, delegate business timing to the framework and keep one system entry. These patterns have held up across fifteen years of production work on Ubuntu VPS infrastructure in Nepal and abroad.

If silent cron failures are costing you backups, billing runs, or queue processing, contact me for a scheduler audit. You can also send a message through my contact page with your crontab and syslog snippet. Explore full-stack development and server support services for end-to-end application and infrastructure work, or review the essential Ubuntu terminal commands reference for day-to-day server operations.

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

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: