The classic pain: npm run dev is perfect locally. Deploy to Vercel / Netlify / Fly.io / Render — build fails, runtime crashes, or the page seems to load but every feature is broken. “Works on my machine” is the oldest joke in the book, but the root cause is always the same family: implicit local assumptions don’t hold in production.
The fix isn’t guessing — it’s systematically diffing local vs prod across env vars / runtime version / filesystem / network / build process.
Common causes
Ordered by hit rate, highest first.
1. Env vars not set in production
Most common. Your local .env has DATABASE_URL, STRIPE_SECRET_KEY, but the deploy platform doesn’t — process.env.X === undefined at runtime.
How to spot it: Prod logs show Cannot read property X of undefined or Invalid URL.
2. Node / Python version mismatch
Local Node 22, prod Node 18 (platform default). Newer ES2023+ syntax or APIs like Array.prototype.toSorted don’t exist on 18 — runtime crash.
How to spot it: Prod logs show X is not a function or SyntaxError.
3. Filesystem differences (case sensitivity, path separators)
// macOS / Windows: case-insensitive
import './components/Button'; // ✅ even if file is button.tsx
// Linux prod: case-sensitive
import './components/Button'; // ❌ Cannot find module
How to spot it: Prod build errors “Module not found” but local works.
4. Local-only services (filesystem, in-memory state)
Locally you do fs.writeFile('./uploads/x.jpg'). On serverless platforms (Vercel / Cloudflare Workers) the filesystem is read-only — write throws.
How to spot it: EROFS: read-only file system or similar.
5. Dev server vs prod build behave differently
Vite / Webpack dev mode has HMR, looser parsing. Prod build tree-shakes, minifies, SSRs. Code that works in dev can break in prod build.
How to spot it: Run npm run build && npm start locally — does it reproduce?
6. CORS / cookie domain / third-party keys
localhost:3000 vs yourdomain.com hit different CORS rules, different cookie SameSite behavior, different allowed-origin lists. OAuth was using test keys in dev and still is in prod.
How to spot it: Network shows CORS error / 401 / 403 in prod but works in dev.
7. Timezone / locale
date.toString() shows “Beijing time” locally, prod server is UTC — every time display is off by 8 hours.
How to spot it: Time-related features are weird in prod.
Shortest path to fix
Step 1: Run the prod build locally
90% of “local works, prod doesn’t” reproduces here:
# Next.js
NODE_ENV=production npm run build && npm start
# Vite
npm run build && npm run preview
# Generic
NODE_ENV=production node server.js
If local prod build also fails → it’s a build / code issue, not deployment. Fix it.
Step 2: Diff env vars
# List local
cat .env | sort > /tmp/local-env.txt
# List prod (from platform dashboard, manually)
echo "DATABASE_URL=..." > /tmp/prod-env.txt
sort /tmp/prod-env.txt > /tmp/prod-env-sorted.txt
# Diff
diff /tmp/local-env.txt /tmp/prod-env-sorted.txt
Add missing:
# Vercel
vercel env add DATABASE_URL production
vercel env add STRIPE_SECRET_KEY production
# Or via dashboard
Step 3: Lock runtime version
// package.json
{
"engines": {
"node": "20.x"
}
}
Vercel / Netlify respect engines.node. Cloudflare Pages: set NODE_VERSION=20 in Build settings.
Confirm .nvmrc / .node-version:
20.10.0
Step 4: Fix case sensitivity
# On macOS, list every import and check casing
grep -r "from './" src/ | sort -u | head -50
# Compare against actual files
ls -la src/components/
Force case-sensitive volume (extreme but bulletproof):
diskutil apfs createContainer disk0s2
# Create a case-sensitive volume and move the project there
Or use eslint rules import/no-unresolved + import/case-sensitive.
Step 5: Replace local-only dependencies
Local → Production
================================================
fs writeFile → S3 / R2 / Cloudinary
in-memory cache → Redis (Upstash / Cloudflare KV)
SQLite → Postgres / Turso
local cron → Vercel Cron / Inngest
process.env at boot → remote config / Vault
Step 6: dev / prod behavior parity
// Never fork dev/prod logic
if (process.env.NODE_ENV === 'development') {
// ❌ dev can't catch prod bugs
}
// Instead:
const useMock = process.env.USE_MOCK === 'true';
// Then run staging with USE_MOCK=false to validate
Step 7: UTC + explicit locale
// Server-side: always UTC
const date = new Date(); // transport as ISO string
// Client-side display: use Intl
new Intl.DateTimeFormat('en-US', { timeZone: 'America/New_York' }).format(date)
Prevention
- Run
npm run build && npm startlocally before every PR — don’t trust the dev server - Validate env vars with
zodat boot — fail-fast on missing rather than at runtime - Lock
engines.nodein package.json; CI usesactions/setup-node@v4 with: node-version-file: .nvmrc - Mac users: run builds inside a Linux Docker container to mimic prod case sensitivity
- Any “local-only” code (fs.write, in-memory state) gets replaced with a cloud equivalent on day 1
- Maintain a preview / staging env identical to prod config; validate every PR there before merge
- Store times as UTC ISO strings; convert to timezone only at display
- Whole company on the same .nvmrc / .python-version — kill “works on my machine”
Related
Tags: #Backend #Debug #Troubleshooting