DEV Community

Cover image for Why Your Deployment Succeeded But Your App Is Broken: Post-Deploy Debugging
Deploynix
Deploynix

Posted on • Originally published at deploynix.io

Why Your Deployment Succeeded But Your App Is Broken: Post-Deploy Debugging

The deployment log shows green. Every hook ran successfully. The release was symlinked, PHP-FPM was restarted, and the deployment status reads "Deployed." But your users are reporting errors, features are not working, or the application is subtly behaving wrong.

This is the most frustrating category of deployment problems because there is no clear error to point at. The deployment itself was mechanically successful — the code is on the server, the process completed without failure. But something in the gap between "code deployed" and "application working correctly" went sideways.

This guide covers the seven most common causes of post-deployment breakage and how to diagnose each one systematically.

The Silent Killer: Cached Configuration

Laravel's configuration caching is a performance optimization that becomes a trap when misunderstood. When you run php artisan config:cache, Laravel reads every configuration file and your .env file, compiles them into a single cached file at bootstrap/cache/config.php, and serves all subsequent config() calls from that cache.

The problem: if you deploy new code that references new environment variables or changed configuration values, but you do not re-cache the configuration, the application continues using the old cached values.

Symptoms:

  • New features that depend on new environment variables fail silently
  • Database connection errors after changing database credentials
  • Third-party API integrations fail after updating API keys
  • The application behaves as if your .env changes never happened

Diagnosis:

Check if a config cache exists and when it was last modified:

ls -la bootstrap/cache/config.php
Enter fullscreen mode Exit fullscreen mode

Compare this timestamp to your deployment time. If the config cache is older than your deployment, your application is running on stale configuration.

Fix:

Add php artisan config:cache to your post-deployment hooks in Deploynix. This should run after the .env file is written and after composer install, but before any commands that depend on configuration values.

The correct order in your deployment hooks:

  1. composer install --no-dev --optimize-autoloader
  2. php artisan config:cache
  3. php artisan route:cache
  4. php artisan view:cache
  5. php artisan migrate --force

Critical caveat: Once you cache configuration, the env() function returns null for any key not present in your config files. If your code calls env('SOME_KEY') directly (outside of a config file), it will break. Always use config() in your application code and define the environment variable mapping in a config file.

Route Cache Mismatch

Route caching works similarly to config caching. php artisan route:cache compiles your routes into a serialized file for faster registration. If you deploy new routes but do not re-cache, the old route definitions remain active.

Symptoms:

  • New routes return 404 errors
  • Changed route parameters cause unexpected behavior
  • Middleware changes do not take effect
  • Route model binding stops working for modified routes

Diagnosis:

Check if the route cache exists:

ls -la bootstrap/cache/routes-v7.php
Enter fullscreen mode Exit fullscreen mode

If it exists and is older than your deployment, routes are stale.

Fix:

Include php artisan route:cache in your post-deployment hooks. Like config caching, this should run after Composer installs dependencies (since routes may reference controller classes).

Important note: Route caching does not work with closure-based routes. If your routes/web.php or routes/api.php contains routes defined with closures instead of controller classes, route:cache will fail with an error. Convert closures to controller methods before enabling route caching.

Queue Workers Running Stale Code

This is perhaps the most insidious post-deployment issue because it creates inconsistent behavior. Your web application serves new code (PHP-FPM was restarted), but your queue workers still run the old code.

Symptoms:

  • Jobs that reference renamed or moved classes fail with "class not found" errors
  • Queue jobs produce results based on old business logic
  • Jobs succeed in the browser (synchronous) but fail in the queue
  • Intermittent errors that seem random but only affect queued operations

Why it happens:

Queue workers boot the Laravel application once and keep it in memory. Unlike PHP-FPM (which re-reads the application for each request, though OPcache may cache the compiled files), queue workers load the entire application framework, service providers, and job classes into memory and hold them there for the lifetime of the worker process.

When you deploy new code, the files on disk change, but the worker's memory still contains the old code. The worker will continue processing jobs with the old application until it is restarted.

Diagnosis:

Check when your queue worker processes started:

ps aux | grep queue:work
Enter fullscreen mode Exit fullscreen mode

Compare the process start time to your deployment time. If workers started before the deployment, they are running old code.

Fix:

Run php artisan queue:restart as a post-deployment hook. This command writes a restart signal to the cache. Each worker checks for this signal between jobs and, when found, gracefully shuts down after completing its current job. Supervisor (or whatever process manager you use) then restarts the worker, loading the new code.

In Deploynix, if you have configured your queue workers as daemons through the dashboard, they are automatically restarted during deployment. This is the recommended approach because it handles the restart timing correctly — after all deployment hooks have run and the new release is active.

View Cache Containing Old Templates

Blade view caching is less commonly discussed but can cause visible issues. When you run php artisan view:cache, Laravel pre-compiles all Blade templates into PHP files. Even without explicit caching, Laravel compiles Blade templates on first use and caches them in storage/framework/views.

Symptoms:

  • UI changes do not appear after deployment
  • Old text, layouts, or styling persist
  • Blade component changes are not reflected
  • New Blade directives cause template errors

Diagnosis:

Check the compiled views directory:

ls -lt storage/framework/views/ | head -10
Enter fullscreen mode Exit fullscreen mode

If the most recent files are older than your deployment, views are stale.

Fix:

Include php artisan view:cache in your deployment hooks (which first clears old cached views, then pre-compiles all templates). Alternatively, php artisan view:clear removes all cached views, forcing recompilation on next access.

Pre-compiling with view:cache is preferred because it moves the compilation cost to deployment time rather than the first user request.

Missing or Stale Asset Manifest

Modern Laravel applications use Vite for frontend asset compilation. Vite generates a manifest.json file that maps source files to their hashed output filenames. The @vite() Blade directive reads this manifest to generate the correct and tags.

Symptoms:

  • CSS and JavaScript files return 404 errors
  • The page loads without styling
  • JavaScript functionality is missing
  • The Vite manifest error appears in the browser

Diagnosis:

Check if the manifest exists and contains recent entries:

ls -la public/build/manifest.json
cat public/build/manifest.json | head -20
Enter fullscreen mode Exit fullscreen mode

If the manifest is missing, assets were not built. If it exists but references old files, the build ran against old source files.

Fix:

Ensure your deployment hooks include the frontend build:

npm ci && npm run build
Enter fullscreen mode Exit fullscreen mode

npm ci is crucial here, not npm install. npm ci installs exactly what is in package-lock.json without modifying it, which is faster and more deterministic. npm install may update the lock file, potentially introducing version differences.

Alternative approach: Build assets in CI/CD (GitHub Actions, GitLab CI) and deploy the pre-built public/build directory. This avoids installing Node.js on your production server entirely and eliminates build failures caused by server resource constraints.

Environment Variables Present But Wrong

Sometimes the .env file exists and contains all required variables, but the values are wrong. This happens more often than you would think, especially in teams where environment configurations diverge across staging and production.

Symptoms:

  • Application works on staging but fails on production
  • Payment processing fails (wrong API keys)
  • Email sending fails (wrong SMTP credentials)
  • OAuth login redirects to the wrong URL
  • S3 file uploads fail (wrong bucket or region)

Diagnosis:

Use the Deploynix environment editor to review your production environment variables. Compare them with your staging environment, looking specifically for:

  • API keys that are still set to test/sandbox mode
  • URLs that point to staging instead of production
  • Database credentials that reference the wrong server
  • APP_URL set incorrectly (missing https://, wrong domain)

Fix:

Update the incorrect values in the Deploynix environment editor. After updating, you must re-cache the configuration for changes to take effect:

php artisan config:cache
Enter fullscreen mode Exit fullscreen mode

Then restart PHP-FPM to ensure the new configuration is loaded.

Prevention:

Maintain a checklist of environment variables that differ between staging and production. Review this checklist before every major deployment. Deploynix's environment editor makes it easy to compare values at a glance.

Event and Service Provider Cache

Laravel can cache event-to-listener mappings and the list of service providers for faster boot times. Like other caches, these become stale when you deploy new code that changes event mappings or adds new service providers.

Symptoms:

  • New event listeners do not fire
  • New service providers are not loaded
  • Recently registered middleware does not execute
  • Scheduled tasks defined in new service providers do not run

Diagnosis:

Check the event cache:

ls -la bootstrap/cache/events.php
Enter fullscreen mode Exit fullscreen mode

Fix:

Include php artisan event:cache in your deployment hooks. For service providers, running php artisan config:cache handles re-caching the provider list since providers are defined in configuration.

The Complete Post-Deployment Checklist

To prevent all seven categories of post-deployment breakage, your Deploynix deployment hooks should include, in this order:

Install phase:

  1. composer install --no-dev --optimize-autoloader
  2. npm ci && npm run build (if using frontend assets)

Cache phase:

  1. php artisan config:cache
  2. php artisan route:cache
  3. php artisan view:cache
  4. php artisan event:cache

Database phase:

  1. php artisan migrate --force

Post-activation:

  1. php artisan queue:restart

Deploynix executes these hooks in sequence, stopping if any command fails. This ensures you never activate a broken release — if composer install fails, the migration never runs, and the old release stays active.

When the Deployment Hook Succeeds But the Command Did Not Work

There is a subtle edge case worth mentioning: a deployment hook can exit with code 0 (success) even if the command inside it did not achieve its intended effect. For example, php artisan migrate --force exits successfully even when there are no migrations to run. This is correct behavior, but it means you cannot rely on exit codes alone to verify that everything is working.

After deployment, verify your application manually:

  • Load the homepage and check for obvious rendering issues
  • Test the login flow
  • Submit a form that triggers a queue job
  • Check the Deploynix monitoring dashboard for error rate spikes

Deploynix's health alerts will notify you of server-level issues (high CPU, memory exhaustion, disk full), but application-level verification requires human eyes or automated smoke tests.

Using Deploynix Rollback as a Diagnostic Tool

When something is broken and you are unsure whether the cause is new code or infrastructure, Deploynix's rollback feature is your fastest diagnostic tool. Roll back to the previous release. If the problem disappears, it is a code issue. If the problem persists, it is infrastructure — environment variables, database state, or server configuration.

This binary test saves enormous time. Instead of combing through hundreds of lines of diff, you immediately narrow the problem to one of two categories.

Conclusion

A successful deployment is necessary but not sufficient for a working application. The gap between "code on the server" and "application functioning correctly" is where caches, workers, and environment configuration create subtle failures.

The fix is not complicated: a well-ordered set of post-deployment hooks handles the vast majority of cases. Configure them once in Deploynix, and every deployment automatically clears and rebuilds caches, restarts workers, and ensures your application runs on the code you just shipped.

When something still goes wrong, the diagnostic approach is systematic: check caches, check workers, check environment, check assets. The answer is almost always in one of these four areas.

Top comments (0)