hellogerard/jobby
Jobby is a PHP cron job manager that runs multiple scheduled jobs from one crontab entry, with built-in locking to prevent overlapping runs, logging, error email notifications, and per-job configuration.
Jobby's job is narrow: it turns one crontab entry into a config-driven scheduler. Get those two pieces wired and you're productive immediately.
composer require hellogerard/jobby.<?php
// config/jobs.php
return [
'nightly-report' => [
'command' => 'php /var/www/app/artisan reports:nightly',
'schedule' => '0 2 * * *',
'output' => '/var/log/app/nightly-report.log',
],
'cache-warm' => [
'command' => 'php /var/www/app/artisan cache:warm',
'schedule' => '*/15 * * * *',
'enabled' => true,
],
];
* * * * * cd /var/www/app && php vendor/bin/jobby -c config/jobs.php >/dev/null 2>&1
(If your install doesn't expose vendor/bin/jobby, the package also supports the programmatic form: a small executable that does new Jobby(); $jobby->add(...); $jobby->run();. Same idea, one entrypoint.)
output log file after a run, and run the entrypoint manually as the same user. Jobby only runs jobs whose cron expression matches the current minute, so a * * * * * job is the fastest way to confirm end-to-end behaviour.First real use case: replace an existing bare crontab line (nightly report, cache warm, queue prune) with a Jobby entry. You immediately gain a log file, overlap protection, and a single place to look when "did the cron run?" comes up. Once one job is migrated, migrating the rest is copy-paste.
If you're on a modern Laravel app, note the obvious: schedule:run already does most of this. Jobby earns its keep when you have non-Laravel PHP, a mixed fleet of shell + PHP jobs, need per-job OS user switching, or want email alerts without building them.
Config-as-code with env awareness. Return the array from jobs.php and build it with env()/getenv() so enabled, maxRuntime, and output paths differ per environment without editing cron. Keep the file in version control and deploy it with the app.
return [
'sync-orders' => [
'command' => 'php /var/www/app/artisan orders:sync',
'schedule' => '*/5 * * * *',
'output' => '/var/log/app/sync-orders.log',
'maxRuntime' => 240,
'enabled' => getenv('APP_ENV') !== 'local',
'description'=> 'Pull new orders from the ERP',
],
];
Multiple schedules per job. schedule accepts an array, which beats duplicating a job just to run it twice a day:
'schedule' => ['0 8 * * *', '0 18 * * *'],
Wrap artisan, don't reimplement it. For Laravel apps the clean pattern is: write a console command that does the work and can be run interactively, then register it in Jobby as a one-liner command. You keep testability via php artisan my:command, and Jobby only handles scheduling concerns.
Use callables for tiny in-process tasks. If a job is a few lines of PHP (pruning a temp dir, busting a cache), a closure avoids shelling out:
'flush-tmp' => [
'command' => function () { /* ... */ },
'schedule' => '0 3 * * *',
],
Keep them small — a callable runs inside the same PHP process as the Jobby tick, so memory and long runtimes affect everything else in that tick.
Per-job environment. Set environment on a job to scope env vars (e.g. APP_ENV, queue names), rather than exporting them globally in the crontab.
User switching. For jobs that must run as a different OS user (e.g. www-data for file ownership reasons), set user on that job and run the Jobby cron entry as root or via a narrowly-scoped sudo rule. Keep the number of root-run jobs as small as possible.
Toggle without redeploys. Because enabled is read from config, you can flip a job off by editing the deployed config, or by env var if you built it in. Prefer env for anything ops needs to touch at 3am.
Integration with monitoring. Jobby's output files are your ground truth. Tail them, or ship them to your log pipeline, and alert on "no new lines in N hours" rather than relying on cron alone — cron dying is silent.
cron/crond stops, every job stops. Add a heartbeat check.* * * * * entry launches Jobby every minute. A job that runs long is protected by the per-job lock, but a pathological config can make ticks pile up. Use maxRuntime to bound runaway jobs and keep heavy work in separate entries if needed.PATH is minimal. php may not be the same binary the shell resolved for you. Prefer /usr/bin/php8.2, and set any required env explicitly.date.timezone (or TZ) consistently and remember DST transitions can produce skipped/duplicated runs on hourly-style schedules.mail()/sendmail on the box. If deliverability matters, log to files and forward with a real alerting system; don't make email your only signal./dev/null. That's intentional in the recommended crontab line — just make sure every job has an output path so failures actually land somewhere.sudo -u www-data php vendor/bin/jobby -c ...), and if the tool offers a debug/foreground flag and a "run just this job" filter, use them — don't debug by waiting for the next scheduled window.composer.json. It's a small, low-churn package, but it's not actively maintained, so treat upgrades of PHP as a reason to re-test it.app/Console/Kernel.php (or the newer routes/console.php) or in the crontab, you'll get silent duplicates. Jobby should be the only scheduler for jobs it owns.How can I help you explore Laravel packages today?