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 not auto-updated.
Last update: 2026-09-07 23:53:11 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.
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.