Deployment and production hardening
Environment configuration, config caching done safely, Supervisor-managed queues, zero-downtime releases, health checks and log handling.
Configuration and caching
composer install --no-dev --optimize-autoloader --classmap-authoritative
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
php artisan queue:restart
php artisan horizon:terminate
php artisan storage:linkconfig:cachereads.envonce, so anyenv()call inside application code returns null afterwards. Onlyconfig/*.phpfiles may callenv().APP_DEBUG=falseandAPP_ENV=productionin production, always - debug mode exposes the environment on any exception page.migrate --forceis required non-interactively; run it before switching traffic so a failure does not leave a half-released app.queue:restartsignals workers to exit after the current job, letting the supervisor start them with the new code.
⚠️
Cache the config, then verify it. A single
env() call in a controller is a production-only bug that works perfectly in local development, because local never caches config.Zero-downtime releases
# atomic release with a symlink switch
releases/20260918120000/ <- new code, composer install, caches built
shared/.env storage/ uploads/
current -> releases/20260918120000
# switch
ln -sfn releases/20260918120000 current
# then reload php-fpm so OPcache picks up the new paths| Step | Why it matters |
|---|---|
| Build caches in the new release directory | The current symlink serves the old, working version until the switch |
Keep storage/ in shared | Logs and uploads survive the switch |
| Reload PHP-FPM after the switch | OPcache is keyed by path; a reload avoids serving mixed code |
| Health check before switching | Catches a broken build before users see it |
| Keep the last two releases | Rollback is another symlink switch |
// a meaningful health endpoint
Route::get('/health', function () {
DB::select('select 1');
Cache::store('redis')->get('health-probe');
return response()->json(['status' => 'ok', 'release' => config('app.release')]);
});Workers, logs and hardening
; supervisor queue worker
[program:laravel-worker]
command=php /var/www/app/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
numprocs=4
autostart=true
autorestart=true
stopwaitsecs=3600
user=www-datastopwaitsecsmust exceed the longest job timeout, or a deploy kills work mid-flight.- Log to stderr in containers and let the platform collect it; on a VM, rotate with logrotate because
dailylogs still fill a disk. - Set
SESSION_SECURE_COOKIE=true,SESSION_SAME_SITE=lax, and serve only over TLS. TrustProxiesmust be configured, or rate limiting andsecurecookie detection see the load balancer's IP.- Add
php artisan schedule:runto cron - exactly one cron entry, and useonOneServer()when several instances exist.
FAQ
Why did my configuration stop being read after caching?
Because
env() is called somewhere outside the config directory. Move every environment lookup into a config/ file, reference it with config('services.x.key'), and clear the cache before re-testing.How do I deploy without dropping queued work?
Deploy the code first, let in-flight jobs finish against the old payloads, then restart workers. Never change a job's constructor signature in the same release that you deploy, unless the old signature still deserialises.
Related
Queues, jobs and scheduled tasks Caching, performance and debugging
Last refreshed 2026-09-18.