
Every Laravel deployment error leaves a trace in one of three files, and reading the right one first cuts debugging time from an hour to a minute. The application log at storage/logs/laravel.log catches anything PHP got far enough to throw — exceptions, database errors, missing classes. The web server error log (on a stock VPS, /var/log/nginx/error.log) catches everything that never reached Laravel: upstream failures, socket errors, timeouts. And the PHP-FPM log records pool-level problems like exhausted worker processes that neither of the other two will mention.
The rule of thumb: a Laravel-styled error page or a blank 500 means look at laravel.log; a plain Nginx 502/504 page means the request died before PHP, so look at the Nginx and FPM logs. If your server runs on CloudStick, the Web Application Logs view surfaces the per-site Nginx and Apache error logs directly in the dashboard, so you can check the failure reason without opening an SSH session — useful when a deploy breaks and you are on a machine without your keys.
A blank 500 right after a fresh deploy is almost always one of two things: a missing APP_KEY or unwritable storage directories. The page is blank because APP_DEBUG=false in production hides the stack trace — which is correct, so resist the urge to flip it on and read laravel.log instead. If the log says "No application encryption key has been specified," generate one. If it says "Permission denied" on a path under storage, the files are owned by the wrong user (typically because you ran Composer or artisan as root):
A close cousin is the error "Please provide a valid cache path." It appears when the storage/framework subdirectories are missing — common when storage is excluded from Git and your deploy script created a bare directory. Recreate the expected tree and the error disappears:
A 502 Bad Gateway means Nginx could not talk to PHP-FPM at all, and the most common cause after a deploy or PHP upgrade is a socket path mismatch: the fastcgi_pass line in your Nginx vhost points at one socket (say, /run/php/php8.2-fpm.sock) while the FPM pool now listens on another (/run/php/php8.3-fpm.sock). The Nginx error log makes it obvious with a line like "connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory)." Fix the path on one side, reload Nginx, done.
A 504 Gateway Timeout under load is different: PHP-FPM is reachable but has no free workers. Check the FPM log — if you see "server reached pm.max_children setting, consider raising it," the pool is exhausted and requests are queueing until Nginx gives up:
Raise pm.max_children in the pool config only after checking memory: each worker uses roughly 50–80 MB for a typical Laravel app, so size the pool to fit your RAM rather than copying a number from a forum. On CloudStick servers each site already gets its own isolated FPM pool with a per-site socket at /run/<site>.sock, which removes the shared-pool contention that causes one busy app to starve another.
Composer dying mid-install with "Allowed memory size exhausted" happens on small VPS instances during dependency resolution — the fix is to lift the memory limit for that one command, and to stop installing dev dependencies in production, which shrinks the job considerably:
If the server still runs out of memory, add a small swap file — a 1–2 GB swap turns a hard Composer failure into a slightly slower install on a 1 GB VPS.
The frontend equivalent is "Vite manifest not found at: public/build/manifest.json." It means the production asset build never ran — Laravel looks for compiled assets in public/build, and that directory only exists after npm ci && npm run build. If the build itself fails with cryptic syntax errors, check your Node version first: Vite 5+ requires Node 18 or newer, and an old system Node is the usual culprit on long-lived servers. Run node -v before blaming your code.
"SQLSTATE[HY000] [1045] Access denied for user" during php artisan migrate is a credentials problem, not a Laravel problem: the DB_USERNAME, DB_PASSWORD, or DB_DATABASE values in your production .env do not match what MySQL actually has. Verify by logging in manually with the exact values from the file — mysql -u youruser -p yourdatabase — and remember that if you have cached config, Laravel reads the cache, not the .env file, so a corrected .env changes nothing until you run php artisan config:clear.
The other classic is "Specified key was too long; max key length is 767 bytes," which appears on older MySQL (before 5.7.7) or MariaDB (before 10.2.2) because utf8mb4 indexes on 255-character strings exceed the old limit. The fix is one line in AppServiceProvider:
Never run migrate:fresh on a production database to recover from a failed migration — it drops every table. Fix the failing migration, roll back the specific batch if needed, and always take a database backup before running migrations on a live app.
Mixed-content warnings — the site loads over HTTPS but assets and redirects point to http:// — mean Laravel sits behind a proxy that terminates TLS and does not know the original request was secure. The fix is trusting the proxy so Laravel honors the X-Forwarded-Proto header. In Laravel 11 and 12 that lives in bootstrap/app.php:
Two more errors are really the same error: something changed but the app behaves as if it did not. First, cached config with stale values — once you run php artisan config:cache, edits to .env are invisible until you clear and rebuild the cache (config:clear then config:cache). Second, queue jobs silently running old code — queue workers are long-lived processes that keep the codebase from the moment they booted, so after every deploy you must run php artisan queue:restart to make Supervisor bring up fresh workers. If your workers run under CloudStick's Supervisord Management, the restart is a one-click operation in the dashboard rather than a step you have to remember to script.
Make queue:restart, config:cache, route:cache, and view:cache the last four lines of your deploy script, in that order. If a bug only appears in production but not locally, run php artisan optimize:clear first — nine times out of ten the difference between the two environments is a cache, not the code.
Nearly every error in this article is preventable by making your deploy a fixed script instead of a sequence you type from memory. The script should: pull the new code, run Composer with --no-dev, build assets with npm run build, run migrations with --force, rebuild the config, route, and view caches, restart the queue workers, and reload PHP-FPM. Run it the same way every time — via Git deployment hooks, a CI pipeline, or CloudStick's Git Deployment feature, which triggers the script automatically on push so a step can never be skipped by hand.
When something still breaks, follow the triage order from the top of this page: identify which layer produced the error page (Laravel, Nginx, or FPM), read that layer's log, and match the message against the fixes here. Blank 500 → APP_KEY or storage permissions. Valid cache path → missing framework directories. 502 → socket path. 504 → pm.max_children. Access denied → .env credentials plus a config:clear. Vite manifest → npm run build and Node version. Old behavior after deploy → caches or workers.
Save that mapping somewhere your whole team can see it. The second time an error appears, the fix should take ninety seconds — because you already know exactly where to look.

