Skip to main content

Deploying

php artisan preflight --deploy runs the checks a deploy can answer, the .env and the configuration, and its exit code is the deploy's assurance:

Exit codeMeansThe deploy
0no check failedgoes on; warnings stand in the log
1at least one check failedstops
2no check failed, and at least one could not lookstops: "not checked" is not a pass

A script stops on a non-zero exit only when it is told to. Put set -e at the top, or the deploy prints the finding and carries on.

A Forge deploy script​

set -e

cd $FORGE_SITE_PATH
git pull origin $FORGE_SITE_BRANCH

$FORGE_COMPOSER install --no-dev --no-interaction --prefer-dist --optimize-autoloader
$FORGE_PHP artisan config:cache

$FORGE_PHP artisan preflight --deploy

( flock -w 10 9 || exit 1
echo 'Restarting FPM...'; sudo -S service $FORGE_PHP_FPM reload ) 9>/tmp/fpmlock

$FORGE_PHP artisan migrate --force
$FORGE_PHP artisan route:cache
$FORGE_PHP artisan queue:restart

The order matters in three places. config:cache comes first: with a configuration cached by the previous deploy, Laravel reads no .env, and the checks would judge the old configuration instead of the one about to serve. preflight --deploy runs after composer install, so it checks the code that is about to serve. And it runs before queue:restart, because it records the time of the deploy, and the queue-restart check later holds the workers' restart signal against it: a signal older than the record means the workers run the code from before the deploy.

On a zero-downtime deploy the same line goes into the release directory before it is activated: the checks run against the new release, and a failure leaves the old one serving.

Before a deploy​

The .env checks need nothing but the file, so a pipeline can lint a production file long before it reaches a server:

php artisan env:lint .env.production

env:lint exits with 1 on any finding, warnings included, and with 2 when the file cannot be read. It reads the stage from the file's own APP_ENV, so the production rules hold even though the pipeline itself runs as testing.

The one setting after installing​

A deploy is covered on its own: the mail-transport check fails it when mail would vanish, visible in the deploy tool and independent of the application's mail. In operation, laravel-health delivers what the checks find, and its default delivers by mail alone. If mail breaks after the deploy, the message about it is the one that does not arrive.

So give the operating mode a second road, one that does not depend on the application's mail. Either let Oh Dear pull the results from laravel-health's endpoint, which keeps reporting when the application is down:

// config/health.php
'oh_dear_endpoint' => [
'enabled' => true,
'secret' => env('OH_DEAR_HEALTH_CHECK_SECRET'),
'url' => '/oh-dear-health-check-results',
],

or add Slack to the channels of a failing check:

// config/health.php
'notifications' => [
'notifications' => [
Spatie\Health\Notifications\CheckFailedNotification::class => ['mail', 'slack'],
],
'slack' => [
'webhook_url' => env('HEALTH_SLACK_WEBHOOK_URL'),
],
],

Until one of them is set up, the health-notifications check warns. It never fails the deploy: mail alone is laravel-health's default, and a check that stopped every standard installation would soon be switched off.

The other roads were weighed and left out. Logging next to the mail delivers nothing: the log lives on the server whose state is wrong, and nobody reads it. Sending a test mail on every run fills the inbox, and a provider accepting a message does not prove it arrives. Switching to another channel by itself whenever mail looks broken would mean running a delivery of its own beside laravel-health's.