← All writing

The Bug Was in the Config, Not the Code

Most Laravel production incidents are not code bugs — they are config that was already wrong before the deploy. Here is why deployment readiness belongs in CI and how I built a CLI package that catches those risks before release.


The worst production incidents I have dealt with were not bugs in the code. The code was fine. The deploy was wrong before it ever went out.

APP_DEBUG=true left on, leaking stack traces to the public. A sync queue quietly running jobs inside the request cycle, so a “background” job timed out the request. A migration that was written, committed, and never run. A log mailer in production, so transactional email went to a file nobody read. None of these throw at deploy time. The app boots. The health check returns 200. The problem surfaces hours later, as something subtle and expensive.

What these have in common is that they are all knowable before the deploy. The information is right there in the config. Nobody checked it — because checking it is a manual ritual that lives in someone’s head and gets skipped under deadline pressure, which is exactly when these mistakes are made.

Readiness is a checklist, and checklists should be code

The list of things to verify before a Laravel release is mechanical and nearly identical across projects:

  • Debug off, app key set, app URL correct
  • No pending migrations
  • Async queue, not sync
  • Config and routes cached, views compiled
  • Storage writable, public storage linked
  • A real mailer, not log or array
  • Scheduler wired correctly

A human running this from memory will eventually miss one. The reliable version of a checklist is a program that runs it the same way every time and fails loudly when something is off. That is the whole idea behind Deploy Guard: take the pre-deploy ritual and make it a command.

php artisan deploy:guard

What makes it useful in CI

A readiness checker is only valuable if a pipeline can act on its result, so the design centres on CI.

Meaningful exit codes. 0 for clean, 1 for failures, 2 for warnings when you have opted into --fail-on=warning. A pipeline gates a release on the exit code without parsing any text.

php artisan deploy:guard --ci --fail-on=warning

Four states, not two. Every check reports pass, warning, fail, or skipped. The skipped state is the one people forget to design for: a mail check on an app with no mailer configured is not a failure, it is not applicable. Conflating the two teaches people to ignore red, which defeats the point.

A suggestion on every problem. A failure is not just APP_DEBUG is enabled in a production environment — it is followed by Suggestion: Set APP_DEBUG=false before deploying to production. The diagnosis and the fix travel together, so the person reading the CI log does not have to go look it up.

Environment-aware grading. --env=production evaluates against production rules. A sync queue passes locally and fails in production, from the same check. Teams that genuinely want a production exception set it deliberately in the allow block — sync_queue_in_production, array_cache_in_production — rather than discovering it by accident.

It checks; it never touches

A deliberate constraint: Deploy Guard does not deploy, does not run migrations, does not edit .env, does not inspect server infrastructure. It reads configuration and reports. That read-only posture is what makes it safe to run anywhere — in CI, on staging, even pointed at production config — without worrying that the checker itself changes something.

This is also why it installs as a dev dependency. It is a tool for the release process, not part of the runtime application.

composer require satheez/laravel-deploy-guard --dev
php artisan vendor:publish --tag=deploy-guard-config
php artisan deploy:guard --json

The JSON output is there for the cases where an exit code is not enough — feeding a dashboard, posting to Slack, or building a custom release gate. And when your app has project-specific readiness conditions — a search index that must be built, a feature-flag service that must be reachable — you register custom check classes in config/deploy-guard.php so they run alongside the built-ins.

What I would build next

The checks assume a fairly conventional deployment. Real fleets run Octane, Vapor, containerised workers, and read replicas, where “correct” config differs from the defaults. The extension I want is deployment-profile presets — Vapor, Octane, queue worker, scheduler-only — that adjust which checks apply and how they grade, so less conventional setups fit without every team hand-tuning the allow-list.

The payoff is simple: the class of incident that comes from a config being wrong before the deploy now fails the build at git push, with a specific suggestion, instead of showing up in the production logs an hour later.

The package is open source at github.com/satheez/laravel-deploy-guard.