You pushed your code, ran the deploy script, opened the site — and got a blank 500 error. Laravel deployment errors are one of the most common reasons a working local app refuses to run in production. The causes are almost always the same handful of mistakes: a missing APP_KEY, unwritable storage directories, a stale config cache, skipped migrations, dead queue workers, or a web server pointing at the wrong folder.
This guide walks through each of these errors with the exact error message, why it happens, and the exact commands that fix it — whether you deploy with Laravel Forge, a plain VPS, or shared hosting.
First: Find the Real Laravel Deployment Error
A production 500 page tells you nothing on purpose — with APP_DEBUG=false, Laravel hides the details from your users (which is correct). Your first job is to see the actual exception.
Option one: tail the log. Every Laravel exception lands in storage/logs/laravel.log:
tail -f storage/logs/laravel.log
Option two: temporarily flip debug mode on the server (never commit this):
# In the server's .env, temporarily: APP_DEBUG=true
Reload the page to see the full stack trace, then set it back to false immediately — Laravel’s debug page prints your database credentials. If the app fails to boot at all, Laravel’s built-in health route is also a quick check: /up returns 200 when the app boots and 500 when it doesn’t.
Once you have the real message, match it to one of the errors below.
1. “No application encryption key has been specified”
The error
Laravel throws Illuminate\Encryption\MissingAppKeyException when APP_KEY is empty. This almost always happens on first deploy because .env is never committed to git — your production server literally has no key.
The fix
Generate a key on the server and add it to the server’s .env (or your Forge environment variables):
php artisan key:generate --show # Copy the output, e.g. base64:9f4H... into .env: APP_KEY=base64:9f4H...
Do not copy your local APP_KEY — the key must be unique per environment. If you cached the config before setting the key (very common), clear the cache afterwards:
php artisan config:clear php artisan config:cache
2. Storage and bootstrap/cache Permission Errors
The error
A 500 with one of these in the log:
The stream or file "storage/logs/laravel.log" could not be opened: Failed to open stream: Permission denied Please provide a valid cache path
Laravel’s deployment docs state plainly that the web server process owner must be able to write to the bootstrap/cache and storage directories. This breaks when you upload files as one user (your SSH user, or root) but PHP-FPM runs as another (www-data).
The fix
Give ownership to the web user and set group-write permissions:
sudo chown -R www-data:www-data storage bootstrap/cache sudo chmod -R 775 storage bootstrap/cache
On Forge, replace www-data with forge. On shared hosting where you can’t run chown, 775 via your file manager or FTP client is usually enough. A subtle variant: if you ran php artisan migrate as root, the new laravel.log file becomes root-owned and PHP-FPM can’t write to it afterwards — re-run the chown after running artisan commands as a different user.
3. Your .env Changes Do Nothing (the Config Cache Trap)
The error
You edited .env on the server, but nothing changes — mail still goes to the wrong SMTP host, the DB credentials are still wrong, or calls to env('STRIPE_KEY') return null in controllers.
This is documented behavior: once you run php artisan config:cache, Laravel combines all configuration into a single file and the .env file is not loaded on requests. The env() function then only returns external system-level variables — meaning every env() call outside of your config/ files returns null.
The fix
After every .env or config change on the server, rebuild the cache:
php artisan config:clear php artisan config:cache
And the long-term fix: only call env() inside files in your config/ directory, then read values with config('services.stripe.key') everywhere else. Note that config:cache will throw if your config directory contains closures, so keep closures out of config files.
4. Migration Failures in Production
The error
Typical messages after a deploy:
SQLSTATE[42S01]: Base table or view already exists SQLSTATE[42S02]: Base table or view not found SQLSTATE[HY000] [2002] Connection refused Nothing to migrate
The fix
First, verify the basics: can the app reach the database with the server’s credentials? Connection refused is a wrong DB_HOST/DB_PORT or a firewall issue — not a migration issue. Then check which migrations have actually run:
php artisan migrate:status
Run pending migrations with the force flag (production blocks interactive prompts):
php artisan migrate --force
“Table already exists” usually means a migration ran once without being recorded — on a fresh database you can roll back that single batch (php artisan migrate:rollback) or mark the existing migration as run in the migrations table. Never run migrate:fresh in production; it drops every table. And always back up the database before running migrations on a live site.
5. Queue Workers Not Running After Deploy
The error
Jobs pile up in the jobs table or Redis, emails never send, and nothing appears in queue:failed. The jobs aren’t failing — nobody is processing them.
Queue workers are long-lived processes. When you deploy new code, the old workers keep running the old code until they are restarted. And if no process monitor is watching them, they simply aren’t running at all after a reboot.
The fix
Laravel’s docs require a process monitor (Supervisor) to keep queue:work running. A typical Supervisor config at /etc/supervisor/conf.d/laravel-worker.conf:
[program:laravel-worker] process_name=%(program_name)s_%(process_num)02d command=php /home/forge/example.com/artisan queue:work --sleep=3 --tries=3 --max-time=3600 autostart=true autorestart=true stopasgroup=true killasgroup=true user=forge numprocs=8 redirect_stderr=true stdout_logfile=/home/forge/example.com/worker.log stopwaitsecs=3600
sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start "laravel-worker:*"
On every deploy, gracefully restart all workers so they pick up the new code:
php artisan queue:restart
On Laravel 12+, you can also use php artisan reload, which terminates long-running services — queue workers, Reverb, and Octane — so your process monitor restarts them on the new code. If jobs are failing, inspect them with php artisan queue:failed and retry with php artisan queue:retry all.
6. Composer Autoload Issues (“Class Not Found”)
The error
Class "App\Models\Invoice" not found Class "Facade\Ignition\IgnitionServiceProvider" not found
The fix
On the server, always install with the optimized autoloader and without dev dependencies:
composer install --no-dev --optimize-autoloader
If you deploy by copying files or running composer update instead of composer install, the autoloader can go stale — regenerate it:
composer dump-autoload
The IgnitionServiceProvider not found variant is a cache trap: an old bootstrap/cache/packages.php references a dev package that --no-dev removed. Delete the stale cache files directly before running artisan commands:
rm -f bootstrap/cache/*.php php artisan package:discover php artisan optimize
And never composer update on the server — that command resolves new dependency versions. The server must install exactly what your lock file recorded.
7. Nginx and Apache Pointing at the Wrong Folder
The error
Either a 404 on every route except /, PHP files downloading as text, or — worst case — the app works but .env is readable in a browser.
The fix
Your web server must serve the public/ directory, never the project root. Laravel’s deployment docs explicitly warn that serving from the project root exposes sensitive files. A working Nginx starting point from the official docs:
server {
listen 80;
server_name example.com;
root /srv/example.com/public;
index index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
The two lines that matter most: root ends in /public, and try_files routes everything through index.php — without it, only the homepage works. The final location block denies access to hidden files like .env.
For Apache, point the virtual host’s DocumentRoot at public/, enable mod_rewrite, and make sure Laravel’s shipped public/.htaccess actually arrived on the server (some FTP clients skip dotfiles).
8. “Vite Manifest Not Found”
The error
Vite manifest not found at: public/build/manifest.json
The page loads but has no CSS or JS, because the frontend was never built on the server. This happens when the deploy script skips the Node build step, or when public/build isn’t uploaded.
The fix
npm ci npm run build php artisan optimize
Use npm ci rather than npm install so the build matches your lock file. If you’re deploying via Forge, this belongs in your deploy script; on shared hosting, build locally and upload the public/build directory.
A Deployment Checklist That Prevents All of This
Put this in your deploy script (or your runbook for manual deploys) and the errors above stop happening:
git pullthe release, thencomposer install --no-dev --optimize-autoloader.- Confirm
APP_KEY,APP_ENV=production,APP_DEBUG=false, andAPP_URLare set in the server environment. - Set ownership/permissions:
chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache. - Run
php artisan migrate --force(after a DB backup). - Build assets:
npm ci && npm run build. - Rebuild caches:
php artisan optimize(which caches config, routes, events, and views). - Restart workers:
php artisan queue:restart(orphp artisan reloadon Laravel 12+). - Smoke test: hit
/upand your key pages, then checkstorage/logs/laravel.logfor new errors.
Further Reading & References
- Laravel Deployment documentation — the official guide: Nginx config,
optimize, and thereloadcommand. - Laravel Configuration documentation — why
env()returnsnullafterconfig:cacheand how config caching works. - Laravel Queues documentation — the official Supervisor configuration for queue workers and the
queue:restartcommand. - Laravel Security Best Practices, 2026 Production Checklist — a pre-deploy security checklist:
APP_DEBUG, config caching,--no-dev, andcomposer audit. - Laravel Deployment Best Practices — a practical pre-deployment checklist with a basic and a zero-downtime deploy script.
- Fixing the “No application encryption key has been specified” error in Laravel — video walkthrough of the
APP_KEY+ config cache problem afteroptimize.
Wrapping Up: Preventing Laravel Deployment Errors
If you’re provisioning a fresh server, see our VPS setup guide and server setup walkthrough.



