caprel / laravel-ops
Install and diagnose the deployment of a Laravel application: cron entry, queue worker, log rotation, scanner trap, dependency audit
Requires
- php: ^8.2
- illuminate/console: ^11.0|^12.0
- illuminate/database: ^11.0|^12.0
- illuminate/mail: ^11.0|^12.0
- illuminate/support: ^11.0|^12.0
- symfony/process: ^7.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.24
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^11.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-09 08:16:09 UTC
README
make ci answers "is the code sound". This package answers the other
question, the one nothing used to ask: is what runs on this server
complete?
It carries the deployment commands that used to be copied from one project to the next:
| Command | What it does |
|---|---|
install:doctor | Diagnoses the deployment: environment, dependencies, database, queue, scheduler, worker, assets |
install:cron | Renders and installs the /etc/cron.d entry running the scheduler |
install:supervisor | Renders and installs the Supervisor program running the queue worker |
install:logrotate | Renders and installs the rotation of the logs Laravel does not rotate itself |
install:fail2ban | Renders and installs the filter and jails of the scanner trap |
ops:security-audit | Runs composer audit and npm audit, and mails new advisories |
Installation
composer require caprel/laravel-ops
The commands are available right away. Two things are worth publishing:
# The deployment templates, into deploy/ of the project. Version them there.
php artisan vendor:publish --tag=ops-deploy
# The configuration, only when a path or a repair command has to change.
php artisan vendor:publish --tag=ops-config
Publish config/ops.php whole: the merge with the defaults of the package
happens on the top-level keys only, so a file holding a partial cron section
replaces the whole section instead of completing it.
The templates are templates
Everything under deploy/ carries {{APP_PATH}}, {{PHP}}, {{USER}},
{{LOG_DIR}}, {{APP_SLUG}}: the install:* commands render them with the
values of the running deployment. Never copy one of those files into /etc
with cp, and never hand-edit the generated file:
- Supervisor refuses a program whose
stdout_logfiledirectory does not exist, and it refuses the WHOLE file with it (CANT_REREAD), taking every other program declared there down; - logrotate and fail2ban say nothing at all, and simply never rotate nor ban;
- a hand-edited file in
/etcis reverted by the next deploy.
Every command accepts --print (render to stdout, write nothing), --root
(prefix every target, to inspect a whole tree), --force (replace a target
whose content differs) and --user / --group / --php.
Diagnosing
php artisan install:doctor # readable table
php artisan install:doctor --json # same content for a probe
Three statuses, and a return code that follows: OK, WARN (return code 0),
FAIL (non-zero, so the command plugs into external monitoring as it is).
Every line that is not OK carries the command that repairs it, and that
command is the one of the project: make migrate rather than
php artisan migrate when the Makefile wraps the rights and the --force. A
project that deploys differently overrides ops.fix_commands and the doctor
stops naming a target that does not exist there.
Checks that concern the server keep quiet when the server is not there: no
/etc/cron.d on this machine means the cron check passes with a mention,
because a diagnosis that shouts on a workstation stops being read anywhere.
Cron or Supervisor
The scheduler runs from one of two places, never both:
// config/ops.php
'scheduler' => env('OPS_SCHEDULER', 'cron'), // 'cron' | 'supervisor'
Left on cron, the doctor asks for the /etc/cron.d entry and install:cron
installs it. Set to supervisor, the doctor asks instead for a schedule:work
program pointing at this checkout, and install:cron refuses to run: installing
the entry alongside would make every scheduled task run twice, which on a task
that writes is an incident rather than waste.
What does NOT depend on the key: the single-scheduler check. Declaring one topology does not stop the other from being installed by hand, and that pair is precisely what must never exist, so it is reported either way.
Two things to move along with the key when choosing supervisor:
ops.cron.logpoints at thestdout_logfileof the scheduler program, which is the file whose freshness proves the scheduler still runs;ops.cron.max_age_minutescomes from the most frequent task of the project.schedule:runwrites a line every minute even with nothing due, butschedule:workwrites only when a task actually runs, so the default of five minutes describes cron and nothing else.
Dependency audit
ops:security-audit runs both audits, and mails the result to
ops.security.recipient (OPS_SECURITY_EMAIL), falling back to the address
the application sends its mail from. The same list of advisories is reported
once, not every morning: a daily mail that never changes stops being read
within a week, and the day a critical advisory appears in it nobody notices.
Schedule it in routes/console.php:
Schedule::command('ops:security-audit')->dailyAt('06:00');
An application with its own mail layout points the views at its own, rather than publishing those of the package:
'security' => [
'views' => [
'html' => 'emails.security-audit',
'text' => 'emails.security-audit-text',
],
],
Tests
composer ci # pint + phpstan + phpunit
The suite runs every command against a throwaway deployment built in a
temporary directory, whose deploy/ holds the stubs this package publishes:
what the tests cover is what a project gets.