Most Laravel production incidents come from deployment mistakes, stale caches or state leaking between Octane requests.
A production deploy typically runs composer install --no-dev --optimize-autoloader, php artisan migrate --force, then `php artisan optimize`, which caches config, routes, events and views. Afterwards it reloads PHP-FPM (for OPcache) and runs php artisan queue:restart.
Config caching has a famous trap: once config is cached, .env is no longer loaded, so env() calls outside config/*.php files return null. Always read settings via config('services.stripe.key') in application code.
Other production essentials:
APP_DEBUG=false(debug pages leak secrets) andAPP_ENV=production.- Queue workers supervised by Supervisor or systemd, or Horizon for Redis.
- One cron entry running
php artisan schedule:runevery minute to drive scheduled tasks defined inroutes/console.php; add->onOneServer()with multiple servers. - Sessions, cache and queues on shared backends (Redis or database) when running several servers.
- Health checks on the built-in
/uproute;php artisan down --secret=...for maintenance.
Octane serves the app from long-lived workers (FrankenPHP, RoadRunner or Swoole). The app boots once and handles thousands of requests, often several times faster. But state now persists between requests:
- Don't inject the request, config repository or container into singletons built at boot; they'll keep the first request's values. Resolve them per call, or use
scopedbindings. - Avoid static arrays that grow (memory leaks); watch memory and set
--max-requeststo recycle workers.
Hosted options such as Laravel Cloud, Forge and Vapor automate much of this.
Example
# deploy.sh (simplified, zero-downtime tools do the same steps per release)
set -e
php artisan down --render="errors::503" --retry=15
git pull --ff-only
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan optimize # config, routes, events, views cached
sudo systemctl reload php8.4-fpm # fresh OPcache
php artisan queue:restart # workers finish current job, then reload
php artisan up
# crontab: drive the scheduler
# * * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
# Octane alternative to PHP-FPM
# php artisan octane:start --server=frankenphp --workers=8 --max-requests=1000
# Gotcha after config:cache:
# env('STRIPE_KEY') in a controller -> null
# config('services.stripe.key') -> works (read env only inside config/*.php)Remember
- php artisan optimize caches config, routes, events and views
- After config:cache, env() works only inside config files
- Octane boots once: avoid request state in singletons and watch memory
Watch out for
- Calling env() directly in application code, which returns null once config is cached.