Production, caching and Octane

Laravel · Expert · 16 min

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) and APP_ENV=production.
  • Queue workers supervised by Supervisor or systemd, or Horizon for Redis.
  • One cron entry running php artisan schedule:run every minute to drive scheduled tasks defined in routes/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 /up route; 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 scoped bindings.
  • Avoid static arrays that grow (memory leaks); watch memory and set --max-requests to 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.

Check yourself

  1. After php artisan config:cache, what does env('APP_NAME') return inside a controller?
  2. Under Octane, a singleton is constructed with the current Request in its constructor. What goes wrong?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: laravel.com/docs/12.x/deployment

Builds on

Unlocks

Nothing builds on this yet.