Jonathan Bird Web Development

How to Fix a Laravel 500 Internal Server Error (2026 Guide)

A 500 internal server error is the least helpful thing a Laravel application can show you. It's not one error, it's the generic mask every unhandled failure wears in production: a missing encryption key, a permissions problem, a stale cache, and a down database all look identical from the browser. Which is exactly why the worst way to debug a 500 is to start guessing at fixes.

In this guide, I'll walk you through how to find the real error behind a Laravel 500 first, and then how to fix each of the usual culprits. Whether your site just went down after a deploy, a fresh install won't boot, or you've inherited an app that's blank-screening, I've got you covered in this article.

What is a 500 Internal Server Error in Laravel?

HTTP 500 means "something went wrong on the server and I'm not telling you what". What you actually see depends on where the failure happened and how the app is configured:

  • Laravel's plain "Server Error" page: an exception was thrown, Laravel caught it, and APP_DEBUG=false (correctly) hid the details
  • A detailed exception page with a stack trace: same thing, but APP_DEBUG=true. Great locally, dangerous in production, because the debug page can expose environment values and secrets
  • A completely blank white page or a bare server error page: either PHP died before Laravel could render an error page (a fatal error very early in the boot process: missing vendor directory, wrong PHP version, unwritable log file), or PHP's own display_errors and log_errors are both switched off, so the error happened and nothing recorded it

The single most important thing to understand: the real error, with a file and line number, is almost always already written down somewhere. Your job isn't to guess, it's to go read it.

Step 1: Find the Real Error

Check the Laravel Log

Start here, always:

1tail -n 100 storage/logs/laravel.log

Errors are appended, so the freshest entry is at the bottom. A single stack trace can easily run past 50 lines, so if you can't see the actual message, widen the window or jump straight to the error lines with grep "production.ERROR" storage/logs/laravel.log | tail -n 5. You're looking for the last exception block: the class, the message, and the first few frames of the stack trace. Nine times out of ten this tells you exactly which section of this guide you need.

If the Laravel Log Is Empty, Check the Server Logs

An empty or missing laravel.log is itself a clue: the failure happened before Laravel's logger could run, or Laravel can't write to the file. The web server saw it, though:

1# nginx + PHP-FPM (paths vary by distro)
2tail -n 50 /var/log/nginx/error.log
3tail -n 50 /var/log/php8.3-fpm.log
4 
5# Apache
6tail -n 50 /var/log/apache2/error.log

Fatal PHP errors ("failed to open stream", "syntax error", "Composer autoloader not found") land here.

If There's Nothing in Any Log

Sometimes both logs come up empty because PHP itself is configured to swallow errors: display_errors off, log_errors off, and no error_log path set. Three moves get you visibility:

  1. Run any Artisan command over SSH, even just php artisan about. Boot failures print straight to the console, so the CLI often shows you the exception the browser refuses to
  2. Turn logging on in the web php.ini (log_errors = On plus an error_log path in the PHP-FPM pool config), then reproduce
  3. As a temporary measure on a local or staging copy only, force errors to display at the top of public/index.php, and remove it as soon as you've seen the message:
1ini_set('display_errors', '1');
2error_reporting(E_ALL);

If the app runs in Docker or Kubernetes, remember tail storage/logs/laravel.log may be the wrong place entirely: set LOG_CHANNEL=stderr so errors land in docker logs / kubectl logs where you can actually see them.

Reproduce With Debug Mode, Locally Only

If you can reproduce the problem in a local or staging copy, set APP_DEBUG=true in .env and hit the page again to get the full exception in the browser. Two warnings:

  • Never leave APP_DEBUG=true on a production site. The debug page can leak environment variables, credentials, and user data
  • If nothing changes after editing .env, your config is cached; run php artisan config:clear and try again

Sanity-Check the Environment

1php artisan about

This prints the application environment, debug state, cache statuses, PHP version, and Laravel version in one hit. It's the fastest way to spot "config is cached", "environment says local on a production box", or "this server is running the wrong PHP".

With the real error in hand, jump to the matching cause below.

Common Causes and How to Fix Them

1. Storage and Cache Directory Permissions

Log says: failed to open stream: Permission denied, The stream or file .../laravel.log could not be opened in append mode, or nothing at all (Laravel couldn't write the log).

Laravel must be able to write to storage/ and bootstrap/cache/. After deploys, chown mistakes, or running Artisan commands as root, ownership drifts and the whole site 500s. The short version of the fix:

1sudo chown -R deployuser:www-data storage bootstrap/cache
2sudo chmod -R 775 storage bootstrap/cache

Substitute your actual deploy user and web server group (www-data on Ubuntu). Permissions issues have enough traps (root-owned log files, SELinux, Docker volumes) that I've written a dedicated guide: How to Fix "Failed to Open Stream: Permission Denied" in Laravel.

2. Missing .env or APP_KEY

Log says: No application encryption key has been specified. Or the log is empty because the .env file doesn't exist at all.

.env is (rightly) git-ignored, so a fresh clone or a new server doesn't have one. Without it there's no APP_KEY, and without a key Laravel refuses to boot, because sessions and encrypted cookies depend on it:

1cp .env.example .env
2php artisan key:generate

On an existing production app, don't regenerate a key that already exists: a new key invalidates existing sessions and anything encrypted with the old one. If production lost its .env, restore it from your secrets store or a backup rather than rebuilding it by hand.

3. Stale Config, Route, or View Caches

Log says: references to config values that no longer exist, routes pointing at deleted controllers, or views that don't match the code you just shipped. Classic symptom: "it 500s after the deploy, but the code is definitely right".

Cached files in bootstrap/cache/ and storage/framework/ can outlive the code they were built from. Reset everything:

1php artisan optimize:clear

Then rebuild the caches for production:

1php artisan optimize

One caveat that bites people constantly: once config is cached, .env is not read at runtime, and any env() call outside the config/ directory returns null. If your code calls env() directly in controllers or services, it will work locally (no config cache) and break in production (cached). Move those reads into a config file and use config() instead.

4. Composer Dependencies and Autoloading

Log/server log says: Failed to open stream: ... vendor/autoload.php, Class "..." not found.

The vendor/ directory isn't committed, so it must be built on the server during every deploy:

1composer install --no-dev --optimize-autoloader

If a specific class isn't found after you've added or moved files, regenerate the autoloader with composer dump-autoload. And if Composer itself refuses to run because of platform requirements, that's usually the next cause on this list.

5. PHP Version Mismatch

Log/server log says: Composer detected issues in your platform, syntax error, unexpected ..., or calls to undefined functions from newer PHP versions.

Laravel 13 requires PHP 8.3 or higher. Two traps here:

  • The server simply runs an older PHP than the app needs. Upgrade the PHP-FPM pool (or select the newer version in your hosting control panel)
  • The CLI and the web server use different PHP versions, so composer install and Artisan work fine over SSH while the site 500s (or the reverse). Compare them:
1php -v # CLI version
2php artisan about # what the app runs under CLI

For the web side, check which PHP-FPM socket your nginx/Apache vhost points at. Both paths need to land on a supported version.

A PHP upgrade can also silently drop extensions. If the log complains about could not find driver or undefined functions right after an upgrade, compare php -m against what the app needs (pdo_mysql, mbstring, intl, and friends) and install the missing ones for the new PHP version.

6. The Database Is Unreachable

Log says: SQLSTATE[HY000] [2002] Connection refused, Access denied for user, or Unknown database.

Because Laravel now defaults sessions, cache, and queues to the database, a dead database connection takes down every page, not just the ones that obviously query data. The database side of this has its own dedicated guide: How to Fix "SQLSTATE[HY000] [2002] Connection Refused" in Laravel. The short checklist: is the database server running, are DB_HOST/DB_PORT right for where the app is running from, and is the config cache stale?

7. Missing storage/framework Directories

Log says: Please provide a valid cache path.

Some deployment setups (and overzealous cleanup scripts) end up without the subdirectories Laravel needs inside storage/framework. Recreate them:

1mkdir -p storage/framework/{cache/data,sessions,views}
2sudo chown -R deployuser:www-data storage

Then clear caches and reload. This one is common on fresh clones where .gitignore rules excluded the directories entirely.

8. Frontend Build Artifacts Missing

Log says: Unable to locate file in Vite manifest or Vite manifest not found.

The Blade layout calls @vite(...) but public/build/manifest.json doesn't exist because the deploy never ran the frontend build:

1npm ci && npm run build

Add it to the deploy script so it can't be forgotten. I've covered the variations (wrong entry names, dev server confusion, build output paths) in my Vite manifest not found guide.

9. Web Server Misconfiguration

Symptom: every URL 500s (or 404s) immediately, often on a brand new server, and Laravel's log has nothing.

Three things to verify:

  • The document root must be the public directory, not the project root. Pointing the vhost at the repo root breaks the front controller and exposes files that should never be public
  • nginx needs the standard try_files rule so all requests funnel through index.php:
1location / {
2 try_files $uri $uri/ /index.php?$query_string;
3}
  • Apache needs mod_rewrite enabled and AllowOverride All for Laravel's default .htaccess to work. If you've customised the .htaccess and things broke, restore the stock Laravel one first and re-apply changes gradually. One exception: on locked-down hosts where AllowOverride doesn't permit Options, the stock file's Options -MultiViews -Indexes line is itself the trigger ("Options not allowed here" in Apache's error log), and commenting that line out is the fix

10. Memory and Execution Limits

Log/server log says: Allowed memory size of ... bytes exhausted or Maximum execution time of 30 seconds exceeded.

A request importing thousands of rows or building a huge export can blow past PHP's limits and surface as a 500. Raising memory_limit or max_execution_time in the PHP-FPM pool config is the quick fix; the real fix is usually moving the heavy work onto a queue, or processing data in chunks with Model::query()->chunk() or lazy() instead of loading everything at once.

11. Shared Hosting and cPanel Quirks

Symptom: the app works locally, 500s on a cPanel host, and none of the VPS-flavoured advice above quite applies.

Shared hosting adds its own failure modes on top of everything already covered:

  • PHP version and extensions: use cPanel's "Select PHP Version" (PHP Selector) to pick 8.3 or newer for Laravel 13, and tick the extensions Laravel needs (pdo_mysql, mbstring, intl, and whatever your packages require). Hosts default to conservative versions
  • Where the errors actually are: check cPanel's Metrics, then Errors, and look for an error_log file that PHP drops next to the failing script (usually inside public_html). That file is often the only record you get
  • The public_html wiring: shared hosts serve public_html, not your app's public/ directory. Either point the domain's document root at public/, or symlink public_html to it. If you've copied public/'s contents into public_html instead, the two require paths in index.php must be updated to point at your app folder's vendor/autoload.php and bootstrap/app.php; a half-finished version of this move is one of the most common cPanel 500s
  • Conflicting .htaccess files: a leftover .htaccess in public_html (from a previous site or the host's defaults) can fight Laravel's own rewrite rules
  • Disabled PHP functions: hosts commonly disable proc_open and symlink, which breaks storage:link and some tooling with a 500. Enable them in the panel if allowed, or ask the host

If the host can't give you PHP 8.3+ with the required extensions, no amount of application debugging fixes that; it's a hosting decision.

Debugging Beyond the Logs

Use the Health Route

New Laravel apps register a health check endpoint at /up that returns 200 when the app boots cleanly and 500 when it doesn't (apps upgraded from the older Laravel 10 file structure won't have it unless a route was added manually). It's the quickest "is the framework itself healthy" probe, and it's exactly what you should point uptime monitoring at, so you find out about a 500 from an alert instead of a customer.

Watch the Log Live While Reproducing

1tail -f storage/logs/laravel.log

Then hit the failing page in another window. Watching entries appear in real time removes any doubt about which log line belongs to which request.

Diff Against the Last Working State

A 500 that appeared "out of nowhere" almost always correlates with a change: a deploy, a dependency update, a PHP upgrade, a server migration, an expired certificate somewhere. git log on the app, your deploy history, and the server's package logs narrow the window fast. "What changed since it last worked?" solves more production incidents than any debugging tool.

Add Error Tracking Before the Next One

Log files answer "what broke" after the fact; an error tracker (Sentry, Flare, Bugsnag, or similar) tells you the moment it breaks, with the request, user, and stack trace attached. Wiring one up takes minutes and turns the next 500 from an archaeology session into a notification.

Preventing 500s: A Sane Deploy Script

Most production 500s in Laravel come from partial deploys: new code with old dependencies, or new config with old caches. A deploy script that runs the same steps in the same order every time eliminates the whole category:

1composer install --no-dev --optimize-autoloader
2npm ci && npm run build
3php artisan migrate --force
4php artisan optimize
5php artisan queue:restart

Alongside that:

  • Keep APP_DEBUG=false and APP_ENV=production in the production .env
  • Point an uptime monitor at /up
  • Make sure storage/ and bootstrap/cache/ ownership survives the deploy process (deploy user in the web server's group)

Summary

A Laravel 500 error is a symptom, not a diagnosis, and the diagnosis is nearly always sitting in a log file. Here's the quick checklist:

  1. Read storage/logs/laravel.log first, and the web server's error log if Laravel's is empty
  2. Fix permissions on storage/ and bootstrap/cache/ if the log mentions "Permission denied"
  3. Restore .env and APP_KEY if the log mentions the encryption key
  4. Run php artisan optimize:clear whenever a deploy or config change preceded the error
  5. Rebuild dependencies with composer install --no-dev --optimize-autoloader and confirm the PHP version is 8.3+ everywhere
  6. Check the database connection if the log shows SQLSTATE errors
  7. Verify the web server points at public/ with the standard rewrite rules
  8. On shared hosting, check the PHP Selector version, the public_html wiring, and cPanel's error log
  9. Automate the deploy steps so the same mistake can't happen twice

The pattern behind every fix in this guide is the same: stop guessing, read the actual error, and the 500 turns into a specific problem with a specific one-line solution.


Site down or fighting recurring 500 errors? I specialise in Laravel development, debugging production issues, and keeping applications healthy long-term. Get in touch to discuss your project.

Topics

Related services

Syntax highlighting by Torchlight

More articles

How to Fix "SQLSTATE[HY000] [2002] Connection Refused" Error in Laravel 13 (2026 Guide)

The "SQLSTATE[HY000] [2002] Connection refused" error means nothing was listening at the host and port Laravel tried to connect to. This guide covers all the common causes and how to fix them in Laravel 13, including localhost vs 127.0.0.1, Docker and Sail service names, custom ports, stale config caches, and CI pipelines.

Read article

How to Fix "The GET Method Is Not Supported for This Route" Error in Laravel 13 (2026 Guide)

The "GET method is not supported for this route" error means the URL matched a route, but the HTTP method did not. This guide covers all the common causes and how to fix them in Laravel 13, including form method spoofing with @method, logout links, redirects that silently turn POST into GET, API verb mismatches, and stale route caches.

Read article

Your website should be a business asset, not a headache

For over 15 years I've helped Australian enterprise, government, and not-for-profit organisations ship websites that are fast, secure, and accessible. Get in touch to discuss your project today.

Planning a project?

Get a quote
Get a quote