Search by

caprel / laravel-ops

Eric-CAP-REL

Install and diagnose the deployment of a Laravel application: cron entry, queue worker, log rotation, scanner trap, dependency audit

Package info

inligit.fr/cap-rel/laravel/laravel-ops

pkg:composer/caprel/laravel-ops

Statistics

Installs: 16

Dependents: 0

Suggesters: 0

v1.2.1 2026-09-08 20:27 UTC

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:

CommandWhat it does
install:doctorDiagnoses the deployment: environment, dependencies, database, queue, scheduler, worker, assets
install:cronRenders and installs the /etc/cron.d entry running the scheduler
install:supervisorRenders and installs the Supervisor program running the queue worker
install:logrotateRenders and installs the rotation of the logs Laravel does not rotate itself
install:fail2banRenders and installs the filter and jails of the scanner trap
ops:security-auditRuns 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_logfile directory 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 /etc is 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.log points at the stdout_logfile of the scheduler program, which is the file whose freshness proves the scheduler still runs;
  • ops.cron.max_age_minutes comes from the most frequent task of the project. schedule:run writes a line every minute even with nothing due, but schedule:work writes 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.