Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

How to Fix the Most Common Laravel Deployment Errors

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:

  1. git pull the release, then composer install --no-dev --optimize-autoloader.
  2. Confirm APP_KEY, APP_ENV=production, APP_DEBUG=false, and APP_URL are set in the server environment.
  3. Set ownership/permissions: chown -R www-data:www-data storage bootstrap/cache && chmod -R 775 storage bootstrap/cache.
  4. Run php artisan migrate --force (after a DB backup).
  5. Build assets: npm ci && npm run build.
  6. Rebuild caches: php artisan optimize (which caches config, routes, events, and views).
  7. Restart workers: php artisan queue:restart (or php artisan reload on Laravel 12+).
  8. Smoke test: hit /up and your key pages, then check storage/logs/laravel.log for new errors.

Further Reading & References

Wrapping Up: Preventing Laravel Deployment Errors

If you’re provisioning a fresh server, see our VPS setup guide and server setup walkthrough.

Leave a Comment