A Laravel application for running scheduled tasks safely across one or more servers. It uses Redis locks to prevent duplicate and overlapping runs.
The project includes one example task:
system.scheduler-heartbeat. It runs every five minutes and writes a heartbeat
to the database.
- PHP 8.4.1 or newer (Docker uses PHP 8.5)
- Laravel 13
- Redis 8 with PhpRedis
- SQLite
- Node.js 22 and Vite
- Docker Compose
You need Git, Docker, and Docker Compose.
git clone https://github.com/janmikolas/scheduling.git
cd scheduling
make upThe first start may take a moment. Docker installs the dependencies, creates
.env and the SQLite database, generates the application key, and runs the
migrations.
The application is then available at:
- http://localhost:8000
- http://localhost:8000/up — health check
Check the scheduler:
docker compose exec app php artisan scheduler:diagnose
docker compose exec app php artisan scheduler:tasksStop the project with:
make downThe code is organized by module. Each module may contain four layers:
| Layer | Purpose |
|---|---|
Domain |
Business rules and value objects |
Application |
Classes that perform the application's work |
Infrastructure |
Laravel, database, Redis, and other integrations |
UI |
HTTP controllers, requests, and responses |
Domain and Application code do not depend on Laravel.
app/
├── Shared/Scheduling/ # Scheduler, task contracts, and Redis locks
└── System/ # Scheduler heartbeat example
A scheduled task passes through the application like this:
Laravel Scheduler / manual invocation
→ scheduler:run-task <task-id>
→ occurrence lock
→ execution lock
→ module task
→ Application class
Schedules use Europe/Prague. Technical timestamps are stored in UTC. If Redis
is unavailable, the task is not started because its locks cannot be checked
safely.
Use
SchedulerHeartbeatTask
as a template.
Place it in the module's Infrastructure/Scheduling directory and implement
ScheduledTask.
The definition controls when and where the task runs:
public function definition(): ScheduledTaskDefinition
{
return new ScheduledTaskDefinition(
id: ScheduledTaskId::fromString('reports.daily-report'),
description: ScheduledTaskDescription::fromString(
'Generates the daily report',
),
cron: CronExpression::fromString('0 6 * * *'),
overlapPolicy: OverlapPolicy::preventFor(
LockTtl::minutes(30),
),
executionScope: ExecutionScope::SingleSchedulerNode,
enabled: true,
);
}The execute() method should only call the class that performs the work:
public function execute(ScheduledTaskContext $context): void
{
$this->generateDailyReport->execute();
}Add it to the module's Laravel service provider:
$this->app->singleton(DailyReportTask::class);
$this->app->tag(
[DailyReportTask::class],
'application.scheduled-task',
);Register a new module service provider in bootstrap/providers.php.
docker compose exec app php artisan scheduler:tasks
docker compose exec app php artisan schedule:list
docker compose exec app php artisan scheduler:run-task reports.daily-report| Command | Purpose |
|---|---|
make up |
Build and start all services |
make down |
Stop all services |
make php |
Open a shell in the PHP container |
| Command | Purpose |
|---|---|
make test |
Run all tests with Laravel |
make phpunit |
Run PHPUnit directly |
make coverage |
Generate HTML test coverage |
make phpstan |
Run static analysis |
make phpcs / make phpcsfix |
Check or fix PHP_CodeSniffer rules |
make phpecs / make phpecsfix |
Check or fix Easy Coding Standard rules |
make deptrac |
Check architecture dependencies |
Run a single test suite or file:
docker compose exec app php artisan test --testsuite=Unit
docker compose exec app php artisan test tests/Feature/HealthEndpointTest.php| Command | Purpose |
|---|---|
php artisan scheduler:tasks |
List registered tasks |
php artisan schedule:list |
Show the schedule and next run times |
php artisan scheduler:run-task <task-id> |
Run one task immediately |
php artisan scheduler:diagnose |
Check scheduler health |
php artisan scheduler:mutex:inspect <task-id> --kind=execution |
Inspect a task lock |
Run Artisan commands in Docker with:
docker compose exec app php artisan <command>Scheduler settings are stored in config/scheduling.php. The main environment
variables are:
| Variable | Default |
|---|---|
SCHEDULER_TIMEZONE |
Europe/Prague |
SCHEDULER_LOCK_PREFIX |
scheduler-service:scheduler: |
SCHEDULER_OCCURRENCE_TTL_SECONDS |
10800 |
SCHEDULER_REDIS_HOST |
127.0.0.1 |
SCHEDULER_REDIS_PORT |
6379 |
SCHEDULER_REDIS_DB |
2 |
See .env.example for the complete configuration.