Skip to content

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.

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').

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

Technologies

  • Laravel
  • Deployment
  • Linux
rupesh@np
--:-- NPT
Esc