Journal/ Laravel/ Deployment/ Linux
Common Laravel deployment problems (and how to avoid them)
Blank 500s, storage permissions, stale config cache, silent queues and mixed content — the Laravel deployment errors I see most, with fixes and a checklist.
Full-Stack Developer, Nepal
- Published
- Reading time
- 3 min read
“It works on my machine” is especially true for Laravel. Locally, php artisan serve hides a lot: file permissions, cached configuration, queue workers, HTTPS. Then you deploy and get a blank 500 page.
Here are the problems that come up most often, what causes them, and how to fix them.
1. A blank 500 error with nothing on screen
Cause: APP_DEBUG=false (correct for production) hides the error, and Laravel can’t write its log file either.
Fix: check storage/logs/laravel.log first. If it’s empty or missing, Laravel can’t write there — see the next point. Check the web server’s error log too (/var/log/nginx/error.log), since PHP fatal errors can happen before Laravel boots.
Never “fix” this by setting APP_DEBUG=true on a public site. Debug pages can reveal environment variables and credentials.
2. Storage and cache permissions
Cause: the web server user (often www-data) can’t write to storage/ or bootstrap/cache/.
Fix:
sudo chown -R $USER:www-data storage bootstrap/cache
sudo chmod -R ug+rwx storage bootstrap/cache
Avoid chmod -R 777. It works, but it lets any process on the server write into your application.
3. .env changes are ignored
Cause: configuration is cached. After php artisan config:cache, Laravel no longer reads .env at runtime.
Fix: run php artisan config:clear or rebuild the cache after every .env change:
php artisan config:cache
A related trap: calling env() directly outside config files. Once config is cached, env() returns null. Always read values through config('services.x.key').
4. The storage link is missing
Cause: uploaded files live in storage/app/public, but the public URL needs a symlink from public/storage.
Fix: php artisan storage:link on the server. On shared hosting where symlinks aren’t allowed, configure a disk that writes directly to a public directory instead.
5. Missing PHP extensions
Cause: the server’s PHP build lacks extensions your app or packages need — commonly gd or imagick for image processing, intl, bcmath, zip or pdo_pgsql.
Fix: compare php -m locally and on the server, and declare requirements in composer.json so Composer warns you at install time:
"require": {
"php": "^8.3",
"ext-gd": "*",
"ext-intl": "*"
}
Also remember that the CLI and PHP-FPM can use different PHP versions and php.ini files on the same server.
6. Queued jobs never run
Cause: QUEUE_CONNECTION is database or redis, but no worker is running. Emails, notifications and image processing silently pile up.
Fix: run workers under a process manager like Supervisor:
[program:app-worker]
command=php /var/www/app/artisan queue:work --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
user=www-data
numprocs=2
stopwaitsecs=3600
And restart workers on every deploy so they pick up new code: php artisan queue:restart.
7. Scheduled tasks don’t fire
Cause: Laravel’s scheduler needs one cron entry to trigger it every minute.
Fix:
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
8. Mixed content and wrong URLs behind a proxy
Cause: behind Cloudflare or a load balancer, Laravel sees plain HTTP requests and generates http:// URLs. Browsers then block assets and forms as mixed content.
Fix: set APP_URL to the https:// address and trust the proxy. In Laravel 11 and later this is done in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: '*');
})
Restrict at: to your proxy’s IP ranges if you’re not behind a service like Cloudflare that already controls inbound traffic.
9. Vite assets return 404
Cause: npm run build wasn’t run on the server (or in CI), so public/build/manifest.json doesn’t exist.
Fix: build assets as part of the deploy, or build in CI and upload public/build. Don’t commit a public/hot file — its presence makes Laravel look for the Vite dev server.
10. Migrations that lock a busy table
Cause: adding a column or index to a large table can lock it and make the site hang during deploy.
Fix: test migrations against a copy of production data, run heavy ones outside peak hours, and always back up the database first. Every migration should have a working down() method — or a written plan for what you’ll do instead.
A deploy checklist
php artisan down --render="errors::503"
git pull origin main
composer install --no-dev --optimize-autoloader
npm ci && npm run build
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 up
Put this in a script, run it the same way every time, and most of the problems above disappear. Deployment should be boring. If it’s exciting, something is missing from the script.
Related projects
- Compressly Pro — Image optimisation product
- GrillPasal — Local-service inquiry platform
Technologies
- Laravel
- Deployment
- Linux