Laravel Deploy Guard
Most Laravel production incidents trace back to a config that was wrong before the deploy — APP_DEBUG on, a sync queue, pending migrations. Deploy Guard is a CLI readiness check that catches those risks in CI before release instead of in production logs after.
Problem
A large share of Laravel production incidents are not code bugs. They are configuration mistakes that were already wrong the moment the deploy went out: APP_DEBUG=true leaking stack traces, a sync queue silently running jobs in the request cycle, pending migrations never run, a log mailer swallowing transactional email, an unwritable storage directory, routes and config left uncached.
None of these throw at deploy time. The application boots. The smoke test passes. The problem surfaces hours later as a leaked secret, a timed-out request, or mail that never arrived. The knowledge of “what to check before shipping” lives in a senior engineer’s head or a stale runbook, and it is applied inconsistently — especially under deadline pressure, which is exactly when mistakes happen.
The questions worth answering before every release are mechanical and identical across projects:
- Is debug mode off and an app key set?
- Are there pending migrations?
- Is the queue async, the cache configured, config and routes cached?
- Can the app write to storage? Is the mailer a real driver?
- Is the scheduler wired correctly?
Constraints
- CLI-first, deploy-never. It checks readiness; it does not deploy, run migrations, mutate
.env, or touch server infrastructure. A read-only inspector is safe to run anywhere, including against production config. - CI as the primary consumer. Output must be machine-readable and exit codes must be meaningful, so a pipeline can gate a release on the result.
- Actionable, not just diagnostic. Every failing check must say what is wrong and what to do about it. A red
[FAIL]with no suggestion just moves the guesswork. - Environment-aware. A
syncqueue is fine locally and dangerous in production. The same check must grade differently depending on the target environment. - Configurable severity. Teams disagree on what is a hard failure versus a warning. The fail threshold and the allow-list of acceptable-in-production conditions must be tunable.
- Extensible. Applications have project-specific readiness conditions. Teams must be able to register custom checks.
Key decisions
A single deploy:guard command with category and key selection. One entry point runs every enabled check. --only and --except scope by category (env, cache, migrations, …) or by individual check key, so a pipeline can run a focused subset per stage.
php artisan deploy:guard
php artisan deploy:guard --only=env,cache,migrations
php artisan deploy:guard --except=mail,queue
Four-state results, not pass/fail. Each check reports pass, warning, fail, or skipped. skipped matters: a mail check on an app with no configured mailer should not be a failure, and conflating “not applicable” with “broken” trains people to ignore the output.
Meaningful exit codes for CI. 0 = no failures (or warnings when allowed), 1 = one or more failures, 2 = warnings exist and --fail-on=warning is set. With failures and warnings both present, it returns 1. This lets a pipeline choose how strict to be without parsing text.
php artisan deploy:guard --ci --fail-on=warning
Environment-aware grading. --env=production evaluates checks against the target environment’s rules. sync queue or array cache passes in development and fails (or warns) in production. The allow config block — sync_queue_in_production, array_cache_in_production, log_mailer_in_production — lets teams opt out of specific production rules deliberately rather than by accident.
Suggestion attached to every non-pass result. A failure reads APP_DEBUG is enabled in a production environment followed by Suggestion: Set APP_DEBUG=false before deploying to production. The fix travels with the diagnosis.
JSON output as a first-class mode. --json emits the full report as structured data for dashboards, Slack notifiers, or release gates that need more than an exit code.
Custom checks via config. Applications register their own check classes in config/deploy-guard.php under custom_checks, so project-specific readiness (a search index built, a feature flag service reachable) runs alongside the built-ins.
Example usage
# Install as a dev dependency
composer require satheez/laravel-deploy-guard --dev
php artisan vendor:publish --tag=deploy-guard-config
# Run all checks
php artisan deploy:guard
# CI: machine-readable, fail the build on warnings too
php artisan deploy:guard --ci --json --fail-on=warning
# Evaluate against production rules from a staging box
php artisan deploy:guard --env=production
Representative output:
Laravel Deploy Guard
Environment: production
Checks run: 22 Passed: 17 Warnings: 3 Failed: 2 Skipped: 0
FAILURES
[FAIL] env.app_debug
APP_DEBUG is enabled in a production environment.
Suggestion: Set APP_DEBUG=false before deploying to production.
WARNINGS
[WARNING] queue.connection
Queue connection is sync in production.
Suggestion: Use database, redis, sqs, or another async queue driver.
Built-in checks span env, database, migrations, queue, cache, storage, mail, filesystem, and scheduler categories.
Outcome
Deployment readiness becomes a repeatable, automated gate instead of a checklist someone might remember to run. The same command works on a laptop before a manual deploy and inside a CI pipeline that blocks a release on a non-zero exit code. Misconfigurations that previously surfaced as production incidents — debug mode on, sync queues, pending migrations, unwritable storage — fail the build at git push with a specific suggestion for the fix. Because it only reads configuration and never mutates anything, it is safe to run in any environment.
What I’d do differently
The checks are environment-aware but assume a fairly conventional Laravel deployment. Real fleets run Octane, Vapor, containerised workers, and read-replica databases where “correct” config differs from the defaults. If I were extending it, I would add deployment-profile presets (Vapor, Octane, queue worker, scheduler-only) that adjust which checks apply and how they grade, so the tool fits less conventional topologies without each team hand-tuning the allow-list.