# Laravel cron job setup for the scheduler

> Laravel needs one crontab line running php artisan schedule:run every minute. How to add it on a server, Forge, Ploi or Docker, and why it may not run.

Laravel needs exactly one cron job: a crontab line that runs `php artisan schedule:run` every minute. Each run checks the tasks in `routes/console.php` and starts the ones due at that minute; you never add a cron line per task.

```text
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
```

Everything below was run on Laravel 13.42 and checked against the [Laravel 13.x scheduling docs](https://laravel.com/docs/13.x/scheduling) on 10 Nov 2026. It applies to Laravel 11 and 12 too.

## Add the crontab line on a server

1. Log in as the user that owns the app and runs PHP-FPM, often `www-data`, `forge` or `ploi`. Not `root`: files the scheduler creates, such as the day's log file, would belong to root and the web server couldn't write to them.
2. Find the PHP binary and the project path:

   ```bash
   which php
   pwd
   ```

3. Open the crontab with `crontab -e` and add one line, with absolute paths:

   ```text
   * * * * * cd /var/www/example.com && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
   ```

   Replace `/var/www/example.com` with your project directory and `/usr/bin/php` with the output of `which php`. Cron runs with a minimal `PATH`, so a plain `php` can find another PHP version than your shell does, or none.

4. Check it was saved: `crontab -l`. For another user: `sudo crontab -u www-data -l`.

The five `*` mean every minute of every hour of every day. The server's timezone doesn't matter for this line, because it runs every minute anyway; task times come from Laravel (see timezones below).

`>> /dev/null 2>&1` throws away the output. While you set things up, send it to a file instead, so a broken run leaves a trace:

```text
* * * * * cd /var/www/example.com && /usr/bin/php artisan schedule:run >> /var/www/example.com/storage/logs/schedule.log 2>&1
```

## Check that it runs

`schedule:list` shows every task with its cron expression and next run:

```bash
php artisan schedule:list
```

```text
 * * * * * php artisan reports:send .. Next Due: 45 seconds from now
 30 2 * * * php artisan backup:run .. Next Due: 9 hours from now
```

`schedule:run` by hand shows what it starts (this is a Mac with Homebrew PHP). The scheduler runs each task with the same PHP binary as `schedule:run` itself, which the second line shows:

```text
 2026-11-10 16:45:14 Running ['artisan' reports:send] .. 455.41ms DONE
 ⇂ '/opt/homebrew/Cellar/php/8.5.10/bin/php' 'artisan' reports:send > '/dev/null' 2>&1
```

`php artisan schedule:test` lets you pick one task and run it now, whatever its schedule.

## Laravel Forge, Ploi and Laravel Cloud

- **Laravel Forge**: turn on the **Laravel Scheduler** toggle of the site. Forge creates the every-minute job with the site's PHP version ([Forge docs](https://forge.laravel.com/docs/sites/laravel)). Jobs you add yourself live under **Processes → Scheduler** of the server or site.
- **Ploi**: on the server, open **Cron jobs** and add a job with the command `php8.5 /home/ploi/example.com/artisan schedule:run`, user `ploi`, frequency every minute ([Ploi docs](https://ploi.io/documentation/server/how-do-i-create-a-cron-job)). Use your site's PHP version and path.
- **Laravel Cloud**: click the environment's App cluster, turn on the **Scheduler** toggle, then save and redeploy. There is no crontab line to write. With more than one replica, use `onOneServer()` or tasks run on every replica ([Cloud docs](https://cloud.laravel.com/docs/scheduled-tasks)).

The three docs pages were read on 10 Nov 2026.

## Docker and Kubernetes

Containers usually have no cron daemon. Two common setups:

- **A scheduler container running `schedule:work`.** Same image and environment as the app, with `php artisan schedule:work` as its command. It stays in the foreground and calls the scheduler every minute. Run exactly one of these, or tasks run once per container.

  ```yaml
  # docker-compose.yml
  scheduler:
    image: your-app-image
    command: php artisan schedule:work
    env_file: .env
    restart: unless-stopped
  ```

- **A Kubernetes CronJob** with schedule `* * * * *` and command `php artisan schedule:run`. Each minute starts a new pod; set `concurrencyPolicy: Forbid` so slow runs don't pile up.

The Laravel docs present `schedule:work` for local development. In a container it does the same job as cron, with one difference: when the process dies, nothing runs until your orchestrator restarts it.

## Tasks that run longer, or on several servers

Three options decide what happens when the cron line fires again before a task is done, or fires on more than one server:

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:send')->everyFiveMinutes()->withoutOverlapping(10);
Schedule::command('invoices:sync')->hourly()->onOneServer();
Schedule::command('analytics:report')->daily()->runInBackground();
```

- `withoutOverlapping()` skips a run while the previous one still holds a cache lock. The lock expires after 24 hours by default; pass minutes, as above, so a killed process doesn't block the task for a day.
- `onOneServer()` lets only the first server that grabs the lock run the task. It needs a `database`, `memcached`, `dynamodb` or `redis` default cache that all servers share. Scheduled closures also need a `->name('…')`.
- `runInBackground()` starts a `command` or `exec` task without waiting for it. Tasks due at the same minute otherwise run one after another, so one slow task delays the rest.

Sub-minute tasks (`everyTenSeconds()` and the like) keep `schedule:run` alive for the whole minute. Add `php artisan schedule:interrupt` to your deploy script, or the old run keeps using the old code until the minute ends.

## Timezones

Laravel decides when a task is due in the app's timezone, `timezone` in `config/app.php` (UTC by default). Set another one per task, or for all tasks with `schedule_timezone`:

```php
// config/app.php
'timezone' => 'UTC',
'schedule_timezone' => 'Europe/Amsterdam',
```

```php
Schedule::command('backup:run')->dailyAt('02:30')->timezone('Europe/Amsterdam');
```

On the nights the clocks change, a task in a daylight saving timezone can run twice or not at all; the Laravel docs advise against timezone scheduling for that reason. Keep the app in UTC, and don't put important tasks between 02:00 and 03:00 local time.

## Why the scheduler isn't running

Most "the scheduler stopped" reports come down to one of these:

| Symptom | Likely cause | Check |
|---|---|---|
| Nothing runs, no errors anywhere | No crontab line on this server, or it's in another user's crontab | `crontab -l` as the app user |
| Works by hand, not from cron | Cron finds another `php`, or the `cd` path is wrong | Use absolute paths; log output to a file instead of `/dev/null` |
| Ran until the last deploy | New server or container without cron; deploy left the app in maintenance mode | `php artisan schedule:list` on the server; tasks skip `php artisan down` unless they have `evenInMaintenanceMode()` |
| One task never runs | A stuck `withoutOverlapping` lock after a crash | `php artisan schedule:clear-cache` |
| A task runs on every server | `onOneServer()` without a shared cache | Your default cache store |
| Runs at the wrong hour | App timezone or daylight saving time | `php artisan schedule:list` |
| Nothing runs since someone paused it | `php artisan schedule:pause` | `php artisan schedule:continue` |

The [scheduler monitoring guide](/guides/monitor-laravel-scheduler/) reproduces the stuck lock and the "it failed but said `DONE`" case, and shows how to catch each.

## Know when it stops

A crontab line can disappear with a server move and nothing complains: no task runs, so no task fails. The usual fix is a heartbeat per task: `->pingOnSuccess($url)` to a cron monitoring service, which alerts when a ping is missing.

We make Lampwick, which checks the schedule without heartbeats: the [Laravel package](/docs/laravel/) reports every scheduled task run, and **Schedule health** learns each task's cron and timezone from its runs. When a task misses its run you get an alert, and when no task runs at all you get one "scheduler not running" alert for the project instead of one per task. Details are in the [Schedule health docs](/docs/cron-health/) and on the [cron monitoring page](/features/cron-monitoring/).

## Next step

Add the crontab line, run `php artisan schedule:list`, then set up [monitoring for the scheduler](/guides/monitor-laravel-scheduler/) so you hear about the night it doesn't run.
