DEV Community

Cover image for 5 Laravel Production Mistakes That Pass Every Local Test
devTalk
devTalk

Posted on Originally published at dev-talk.com

5 Laravel Production Mistakes That Pass Every Local Test

The most expensive Laravel production mistakes are the ones that work perfectly on your laptop. Locally you have one server, no cache files, no load balancer, a debug page you want to see, and a queue worker you restart by hand. Production has the opposite of all of that. Here are five mistakes that behave exactly this way, why they only appear after a deploy, and the fix for each. Everything below was checked against the Laravel documentation and the framework source (tag v13.35.0).

1. Calling env() outside config files

This works locally and returns null in production:

// app/Services/PaymentGateway.php
$key = env('STRIPE_SECRET');   // fine locally, null once config is cached
Enter fullscreen mode Exit fullscreen mode

The Laravel documentation is explicit: once configuration has been cached with config:cache, the .env file is not loaded, and calls to env() for .env variables return null. The framework source shows why: the bootstrapper that loads .env returns immediately when the configuration is cached. Since php artisan optimize runs config:cache (along with the event, route and view caches), most production deploys do this without anyone noticing.

The fix: read env() only inside config/*.php, then use config() everywhere else.

// config/services.php
'stripe' => [
    'secret' => env('STRIPE_SECRET'),
],

// app/Services/PaymentGateway.php
$key = config('services.stripe.secret');
Enter fullscreen mode Exit fullscreen mode

To catch it before production, run php artisan config:cache locally and click through the feature, or run php artisan config:show services.stripe to see the value the app will really use. Remember to run php artisan config:clear afterwards.

2. Leaving APP_DEBUG on

It is the setting you need every day in development, so it is easy to copy the same .env to a server. The documentation states it plainly: in production this value should always be false, and if it is true you risk exposing sensitive configuration values to your end users. An error page with a stack trace and environment details is a gift to anyone probing your site.

# .env on the production server
APP_ENV=production
APP_DEBUG=false
Enter fullscreen mode Exit fullscreen mode

Add a line to your deploy checklist, or a post-deploy smoke test that requests a URL which throws an error and checks the response contains no stack trace.

3. Not trusting your load balancer

Behind a load balancer that terminates TLS, your app receives plain HTTP on port 80. Locally you never see this. In production, Laravel can generate http:// links on an HTTPS site, because it does not know it should trust the forwarded headers. The same problem affects anything that depends on the client IP, such as per-IP rate limiting, since every request can appear to come from the proxy.

Per the documentation, configure trusted proxies in bootstrap/app.php:

use Illuminate\Foundation\Configuration\Middleware;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->trustProxies(at: [
        '10.0.0.0/8',   // your load balancer range
    ]);
})
Enter fullscreen mode Exit fullscreen mode

If you are on a cloud load balancer and cannot know its addresses, the documentation allows '*' to trust all proxies. That is convenient but only safe when the app is reachable exclusively through the load balancer; otherwise a client can send forged forwarded headers directly. If your provider uses a different header (for example AWS ELB), set the headers argument as the documentation describes.

4. Queue workers still running the old code

Queue workers are long-lived processes that keep the booted application in memory, so they do not notice a new release. After a deploy your web requests run the new code while your workers keep running the old code, and jobs fail in ways that look impossible: a job class that no longer exists, a changed constructor, a renamed column. The documentation says to restart the workers during your deployment:

php artisan queue:restart
Enter fullscreen mode Exit fullscreen mode

This tells workers to exit gracefully after the current job, so you need a process manager such as Supervisor to start them again. The restart signal is stored in the cache, so a working cache driver is required. The current deployment documentation also describes php artisan reload, which restarts all long-running services in one command. In the 13.35 source it runs queue:restart and schedule:interrupt, plus any commands registered by packages, and accepts --except to skip some. On Laravel 12 or older, use queue:restart directly.

5. Running things on every server

With one local machine, a scheduled task runs once and a migration runs once. With three web servers, a deploy script and a scheduler can run on all three. Two defenses cover most of it.

Migrations: add --isolated so only one server runs them, with the others skipping safely:

php artisan migrate --force --isolated
Enter fullscreen mode Exit fullscreen mode

Scheduled tasks: mark them to run on a single server:

use Illuminate\Support\Facades\Schedule;

Schedule::command('report:generate')
    ->fridays()
    ->at('17:00')
    ->onOneServer();
Enter fullscreen mode Exit fullscreen mode

Both rely on an atomic lock in the cache, so every server must use the same central cache. For onOneServer(), the documentation requires the default cache driver to be database, memcached, dynamodb or redis. A per-server file cache will not coordinate anything. Closures scheduled with Schedule::call() need a ->name() to be run on one server. If you are on Laravel 13.35 or later, Schedule::alwaysOnOneServer() applies this to every event, with the same cache requirement.

A deploy script that avoids all five

composer install --no-dev --optimize-autoloader
php artisan migrate --force --isolated
php artisan optimize          # caches config, events, routes and views
php artisan queue:restart     # or `php artisan reload` on Laravel 13
Enter fullscreen mode Exit fullscreen mode
Mistake Symptom in production Fix
env() in app code Values are null after config:cache Use config(); env() only in config/
APP_DEBUG=true Stack traces and config shown to users APP_DEBUG=false
Proxies not trusted http:// links, wrong client IP trustProxies(at: ...)
Workers not restarted Jobs run old code queue:restart in every deploy
Multi-server duplicates Tasks and migrations run several times --isolated, onOneServer(), shared cache

References: Configuration, Deployment, Queue Workers and Deployment, Configuring Trusted Proxies and Running Tasks on One Server.


Originally published on DEV Talk.

Top comments (0)