Skip to main content

The checks

Every check has a key. The key names it in laravel-health, under production-preflight.acknowledged, and in every report. A failed check stops preflight --deploy; a warning is printed and lets it through.

The .env file​

Read as text, without booting anything, so env:lint can run these in a pipeline long before a deploy. The reader takes the special cases one by one: a comment line sets nothing, the export prefix is not part of the key, a value may contain =, an empty value counts, and a quoted value that runs over several lines is skipped to its closing quote, so a line of an embedded certificate never reads as a key of its own.

KeyFindsVerdict
duplicate-env-keysa key set on more than one linefail for different values, warning for equal ones
env-example-keysa key of .env.example missing from .env, and the other way roundfail for a key missing from .env, warning for one missing from the example
placeholder-valuesa value that is still a placeholder, a secret copied from the committed example, a required key left emptyfail, in production and on staging
unquoted-hashan unquoted value with a # the application stops reading atfail when the # touches the value, warning after a space
env-bytesa byte order mark, carriage returns, whitespace after an unquoted valuewarning

When a key is set twice, the value of the last line applies. phpdotenv lets a later line overwrite an earlier one and protects only what the real environment held before loading, so the first line has no effect and nothing says so.

The example file comparison runs in both directions at different volumes. A key the example names and .env lacks is the most common go-live mistake: the feature ships, env() answers null, and null is usually a valid value. A key .env sets and the example lacks is drift, and the next installation, set up from the example, then misses it.

Placeholders are listed in production-preflight.placeholders.values, compared without case, and an entry ending in * matches every value it begins. null is not on the shipped list: Laravel's own example file sets REDIS_PASSWORD=null. A key whose name matches production-preflight.placeholders.secret_keys also fails when its value is the one the committed example carries, since everyone who reads the repository knows it. production-preflight.placeholders.required names the keys that must be set and not empty. The stage is read from the file's own APP_ENV where it sets one, so a production file linted in a pipeline is held to the production rule.

phpdotenv starts a comment at the first unquoted #, with or without a space in front of it: APP_NAME=My #1 App loads as My, and THEME=#ff0000 as nothing. Quoting the value keeps the #.

Laravel reads a byte order mark, carriage returns and trailing whitespace correctly; measured against phpdotenv, it drops the mark, ends a line at a carriage return as well, and trims an unquoted value. Everything else that reads the file as text does not: a shell that sources it keeps the carriage return in each value, and a tool that takes a line literally, such as Docker's --env-file, keeps the whitespace. That is why these warn.

The configuration​

These need the loaded configuration and nothing of the host, so they run in the deploy, before any harm is done. They apply in production and on staging.

KeyFindsVerdict
app-debugdebug mode onfail
app-keyno key, a placeholder an old Laravel release shipped, or the key of the committed example filefail
mail-transporta default mailer that delivers nothing: log, array, a mail catcherfail; SMTP to the machine itself warns
health-notificationsalerts in operation that only mail deliverswarning
test-mode-keysStripe or Mollie keys in test mode, a sandbox switch that is onfail
queue-connectiona default queue that discards jobs, or runs them inside the requestfail for null, warning for sync
session-driversessions that last one requestfail
cache-storea default cache that keeps nothing past the requestfail for array and null
secure-cookiea session cookie without the Secure flag on a site that runs over HTTPSfail
debug-toolsTelescope recording, an enabled Debugbar, Ignition loadedfail; Ignition warns
log-levelthe default log channel at debug levelwarning

The cache fails for array as well as null because more than speed hangs on it: the rate limiters count in it, atomic locks live in it, and queue:restart leaves its signal there.

queue-connection warns for sync rather than failing, because on a small installation it is a choice. Acknowledge it there.

mail-transport warns for SMTP to the machine itself: a relay on the same server is a valid setup, and only whoever configured it knows whether that is what it is. A failover mailer is read by its first mailer, since log never fails over; a roundrobin mailer by every mailer.

health-notifications is the second half of the mail check. In operation, laravel-health delivers what the checks find, by mail unless told otherwise, and one of the findings is a mailer that delivers nothing. It warns until a second channel is set up. Deploying says which.

test-mode-keys walks the whole configuration, because a provider's keys stand wherever its package put them, and reports a setting by its path, never its value.

debug-tools asks the application whether a tool's service provider is registered, which is what a --no-dev install leaves out.

laravel-health ships a DebugModeCheck of its own. app-debug reads the stage, so one registration holds on every server, and it runs in the deploy command.

The installation​

These need the server. They do not run in a deploy, and that is the reason they exist: half of these states arise after the go-live.

KeyFindsVerdict
config-cache-fresha cached configuration older than .envfail
queue-restartworkers that run the code from before the last deployfail
pending-migrationsmigrations that have not runfail
writable-directoriesa directory Laravel writes to that is missing or not writablefail
storage-linka storage link that is missing or points elsewherefail over stored files, warning before the first
env-exposurea .env every user of the server can read or write, a .env or .git in the web rootfail
env-over-httpthe site serving .env or .git/config to anyone who asksfail; off unless switched on
dev-dependenciesdevelopment packages installedfail
optimized-autoloaderan autoloader that is not optimizedwarning
php-version-paritythe command line on another PHP version than the web serverfail; needs the binary named
opcacheOPcache off, or checking every file for changesfail for off, warning for timestamps

The first two share a shape: the code is deployed, and something still runs on the state from before. Somebody changed a value on the server and forgot config:cache, so Laravel, which reads no .env while the configuration is cached, runs on the old value. Or the deploy forgot queue:restart, and the workers keep yesterday's classes. Both are invisible and cheap to measure: two modification times, and two timestamps.

queue-restart holds the signal queue:restart writes against the time preflight --deploy recorded. Without a recorded deploy it says it cannot tell.

Some of these cannot answer on their own, and none of them guesses. Whether the site serves .env depends on the web server's configuration, which no file of the application describes: env-over-http asks the site with a request when switched on, and says it did not look otherwise. The PHP of the web server is a binary php-version-parity runs; without one named, or on a host that switched off proc_open, it is not checked. OPcache settings of the web server are invisible from the command line; opcache answers in a web request. In operation shows how to switch each on.

Two entries a reader might expect come from laravel-health itself: ScheduleCheck watches the scheduler with a heartbeat, and OptimizedAppCheck reports a configuration, routes or events that are not cached. Register them beside these checks. Views are not on the list: Blade compiles a template once and keeps it, so view:cache only moves that first compile into the deploy.