Sitemap
Write A Catalyst

Write A Catalyst and Build it into Existence.

API Works on Local but Fails in Production? 7 Real Fixes That Saved My Laravel 12 App (2026 Edition)

5 min readMar 6, 2026

--

Let me tell you a story that still makes me angry.

A few months back, I shipped what I thought was a rock-solid Laravel 12 API. Everything was perfect on my local machine — Postman tests flying, JSON responses clean, file uploads working like a dream and I was over the moon. I pushed to production at 11 PM, patted myself on the back, and went to sleep.

Woke up to 47 Slack notifications. 500 errors. Frontend dead. Clients pissed. Classic “it works on my machine” nightmare that we developers often get.

I’ve been battling Laravel in production since version 6, and this crap still happens if you’re not paranoid. The gap between php artisan serve and a real server is massive — especially in Laravel 12 with its new bootstrap/app.php, stricter caching, and optimized defaults.

I’ve lost hours (and once an entire weekend) to these exact issues. So today I’m giving you the 7 real fixes that have saved my ass repeatedly in 2026. No fluff. No “just Google it.” Real code. Real war stories. And I’m being brutally honest about the dumb mistakes we all make.

Let’s fix your production stuff.

1. Config Caching Is Silently Murdering Your env() Calls (The Silent Killer)

This one makes me want to scream every single time.

You run php artisan config:cache (which Laravel 12 basically begs you to do in production), and suddenly every env(‘SOMETHING’) you wrote in a controller, job, or service returns null. Local works because php artisan serve reloads .env every request. Production? .env gets completely ignored after caching.

Laravel 12 docs are crystal clear on this, but nobody reads them until it’s 3 AM.

The dumb mistake I made (and you probably did too):

// Somewhere stupid like a controller
$key = env('STRIPE_SECRET');

The correct way (Laravel 12 approved):

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

// Then EVERYWHERE else in your app
$key = config('services.stripe.secret');

Fix command sequence (run this every deploy):

php artisan config:clear
php artisan config:cache
php artisan optimize # Laravel 12's magic one-command to rule them all

If you ever touch .env in production again without clearing and recaching, I will personally judge you like our relatives judge us on our bold looks.

2. Your APP_URL Is Still Pointing to localhost (Signed Routes & Asset Hell)

Signed password resets, temporary URLs, Storage::url(), and even mail links break because your production .env still has APP_URL=http://localhost.

I once spent four hours chasing why signed URLs were invalid only in prod. Turned out to be this idiot mistake.

Fix:

env # .env (production) APP_URL=https://api.yourdomain.com

Then make sure your public disk respects it:

// config/filesystems.php
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
],

After changing this:

php artisan config:cache
php artisan storage:link

Do this once and forget it forever.

3. The Infamous Missing Storage Symlink (404 Images & Files in Production)

You upload files locally → they show. Deploy → broken images everywhere. Every. Single. Time.

Laravel 12 changed the default local disk root to storage/app/private (yes, really — check the upgrade guide). If you don’t link properly, you’re screwed.

Fix:

php artisan storage:link

But here’s the critical part most people miss: On many VPS setups (especially when your web server user is www-data or nginx), the symlink gets created but permissions block it.

Run this after every deploy:

php artisan storage:link
chmod -R 775 storage/app/public

Still broken? Create a manual route as backup (I do this on every serious API now):

// routes/web.php or api.php
Route::get('/storage/{path}', function($path) {
return response()->file(storage_path('app/public/' . $path));
})->where('path', '.*');

4. Database Connection Nightmares (Credentials + Strict Mode + Extensions)

Local uses SQLite or a forgiving MySQL. Production throws “Connection refused”, “Access denied”, or mysterious “Specified key was too long” errors because MySQL 8 is strict as my mother.

Get Asfia Aiman’s stories in your inbox

Join Medium for free to get updates from this writer.

Laravel 12 requires PHP >= 8.2 and certain extensions (PDO, Mbstring, etc.). Miss one and good luck.

Make local match production:

// config/database.php
'mysql' => [
'strict' => true,
'modes' => [
'ONLY_FULL_GROUP_BY',
'STRICT_TRANS_TABLES',
'NO_ZERO_IN_DATE',
'NO_ZERO_DATE',
'ERROR_FOR_DIVISION_BY_ZERO',
'NO_ENGINE_SUBSTITUTION',
],
],

Deployment commands:

php artisan migrate --force
php artisan config:cache

And for the love of everything — never hardcode DB credentials. Ever. I will find you.

5. File Permissions on storage/ and bootstrap/cache (The Silent White Screen)

This one usually shows as a blank page or random 500s with zero useful error because APP_DEBUG=false.

The web server user literally can’t write logs or cache files.

The fix that works every time (Ubuntu/Debian):

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

Add this to your deployment script. Every. Single. Time.

I’ve seen senior devs with 10+ years experience waste entire days on this. Don’t be them.

6. CORS & API Routes Not Registered Properly in Laravel 12 (404 City)

In Laravel 12, API routes are no longer magically there. You have to run php artisan install:api and properly register in bootstrap/app.php.

Local works because you’re probably running php artisan serve with fresh routes. Production with route:cache? 404 hell.

Fix in bootstrap/app.php:

->withRouting(
api: __DIR__.'/../routes/api.php',
apiPrefix: 'api', // or whatever you want
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up', // new Laravel 12 health check
)

Then configure CORS properly (no more fruitcake package by default):

// config/cors.php
'allowed_origins' => ['https://your-frontend-domain.com'], // NEVER use * in production
'allowed_origins_patterns' => [],
'allowed_methods' => ['*'],
'allowed_headers' => ['*'],
'allow_credentials' => true,

7. You Forgot to Restart Workers, Octane, or Reverb After Deploy

Your API routes work but queues are running old code. Or Laravel Octane/Reverb is serving stale versions.

Laravel 12 introduced php artisan reload for a reason.

After every single deployment:

php artisan optimize
php artisan queue:restart # or horizon:terminate if using Horizon
php artisan reload # New in Laravel 12 — restarts Octane/Reverb

If you’re on Supervisor, make sure your deployment hook restarts it. Otherwise you’re living in yesterday’s code.

My Brutal 2026 Production Deployment Checklist (Copy-Paste This)

Before you hit deploy, run this or I swear you’ll regret it:

composer install --optimize-autoloader --no-dev
php artisan optimize
php artisan storage:link
php artisan migrate --force
php artisan queue:restart
php artisan reload
php artisan config:cache
chmod -R 775 storage bootstrap/cache

Set APP_DEBUG=false and APP_ENV=production.

Hit your new health check: https://yourapi.com/up

Final Rant

Stop treating production like it’s just a bigger version of your local machine. Laravel 12 made a lot of things faster and cleaner, but it also became less forgiving of sloppy deployment habits.

Do these 7 things religiously and you’ll sleep like a baby.

I’ve turned this exact checklist into a GitHub Action that runs automatically — zero manual mistakes since then.

If this saved you even one headache, do me a favor:

→ Follow for more brutal Laravel 12 production truths → Drop a comment with your worst “works locally but not prod” story → Share this with that one teammate who still deploys by dragging folders

Now go fix that production API before your boss wakes up.

You got this.

--

--

Asfia Aiman

Written by Asfia Aiman

Building & scaling Laravel apps in Pakistan. I write about real production problems: APIs, queues, security, AI integrations.

Write A Catalyst

Published in Write A Catalyst

Write A Catalyst and Build it into Existence.