Netlify build fails but your app works locally
You built an app with Lovable, Bolt, v0, or Cursor. On your machine npm run dev works perfectly. Then Netlify (or Vercel) tries to build it and fails. "It works on my machine" is not a coincidence here. The build machine is a clean Linux box that installs everything from scratch and runs the strict production build, and your laptop is neither clean nor strict. That gap has six known causes, and your deploy log tells you which one you have.
First: read the right line of the log
Open the failed deploy in Netlify (Deploys, click the failed one) or Vercel and read the full log. Do not read only the last line. The last line is almost always a generic wrapper like "Build script returned non-zero exit code: 2" or "Command failed with exit code 1", which tells you nothing. Scroll up to the first line that says Error, error TS, Cannot find module, or not found. That first error is the real one. Everything after it is fallout.
Then reproduce it locally the way the build machine sees it, in a clean clone:
git clone <your-repo-url> fresh-copy
cd fresh-copy
npm ci
npm run build
If this fails on your machine too, you can fix and retest in seconds instead of pushing guesses at Netlify. If it passes locally but still fails on Netlify, you are looking at cause 1 (file name case) or cause 4 (Node version), because those are the two things a clean clone on a Mac still cannot reproduce.
Cause 1: file name case (the Mac vs Linux trap)
Log line: Cannot find module './Button' or Could not resolve "./components/Header", for a file you can see right there in your project.
Your Mac's filesystem is case-insensitive, so import Button from './Button' happily finds button.tsx. Netlify and Vercel build on Linux, which is case-sensitive, so the same import fails. AI tools produce this constantly because they rename components across edits and the mismatch never surfaces locally.
Fix: make the import match the file name exactly, capital for capital. Search your project for the module named in the error and compare spelling character by character.
One extra trap: git itself may have recorded the wrong case, so the file looks right on your disk but is committed as button.tsx. A plain rename does nothing because git considers it the same file. Force git to see the case change with a two-step rename:
git mv src/components/button.tsx src/components/Button2.tsx
git mv src/components/Button2.tsx src/components/Button.tsx
git commit -m "Fix file name casing"
Push and redeploy. You can catch every future instance by setting git config core.ignorecase false in the repo.
Cause 2: missing environment variables at build time
Log line: something like TypeError: Cannot read properties of undefined during a prerender or "Collecting page data" step, or supabaseUrl is required, or a crash inside a client library's constructor.
Your .env file lives on your laptop and is (correctly) not committed, so the build machine has none of those values. Vite bakes import.meta.env.VITE_* in at build time, and Next.js reads process.env.NEXT_PUBLIC_* during prerendering, so an undefined value can crash the build itself, not just the running app.
Fix: set every variable your app needs in the host dashboard. On Netlify: Site configuration, then Environment variables. On Vercel: Project Settings, then Environment Variables. Copy names exactly from your local .env, including the VITE_ or NEXT_PUBLIC_ prefix. Then trigger a new deploy. Adding variables does not rebuild anything by itself, and the old failed build never sees them.
Cause 3: build tools in devDependencies (or the reverse)
Log line: sh: 1: vite: not found, tsc: not found, or Cannot find module 'tailwindcss'.
Your package.json has two dependency lists. If NODE_ENV=production is set as an environment variable, or the install command is npm ci --omit=dev or npm install --production, the build machine skips devDependencies entirely. If vite, typescript, or tailwindcss live there, the tools that run the build were never installed.
Fix, pick one:
- Remove
NODE_ENV=productionfrom the host's environment variables (you almost never need to set it yourself; the build sets it where it matters), and make sure the install command is plainnpm ciornpm install. - Or move the build tools into
dependencies:npm install vite typescript tailwindcss --save-prod(adjust to the packages actually named in your errors).
If the missing module is specifically Tailwind, there is a deeper version to this problem: the Tailwind missing module guide.
Cause 4: Node version mismatch
Log line: error: Unsupported engine, The engine "node" is incompatible with this module, a crash mentioning node: internals, or a syntax error on modern code that your local Node handles fine.
Your laptop might run Node 22 while the build image defaults to something else, and some packages hard-require a version range. Pin the version you actually use locally (check with node --version). Three ways, any one works:
- Add a
.nvmrcfile in the repo root containing just the version, for example22. Netlify and Vercel both respect it. - On Netlify, set the environment variable
NODE_VERSIONto22in Site configuration, then Environment variables. - Add an engines field to
package.json:"engines": { "node": "22.x" }. Vercel uses this to pick the version.
Commit, push, redeploy.
Cause 5: lockfile out of sync, or peer dependency conflicts
Log line: npm ci can only install packages when your package.json and package-lock.json are in sync, or npm error code ERESOLVE with "unable to resolve dependency tree".
This one hides locally because your node_modules folder is months of accumulated installs, and whatever is in there keeps working even when the lockfile no longer matches package.json. The build machine starts from nothing, so the mismatch or the conflict bites immediately. AI tools cause it by editing package.json directly without running an install.
Fix for the sync error: run npm install locally to regenerate the lockfile, then commit the updated package-lock.json and push.
Fix for ERESOLVE: the log names the two packages that disagree. Update the one that is behind (often a plugin that has not caught up with your framework's major version). If you need the deploy green today, adding legacy-peer-deps=true to a .npmrc file in the repo root makes npm tolerate the conflict, but treat that as a bandage and fix the versions when you can.
Either way, verify with the clean-clone npm ci and npm run build from the top of this page before pushing.
Cause 6: TypeScript errors that dev mode ignores
Log line: error TS2339: Property 'x' does not exist on type, error TS2322: Type 'string' is not assignable, or a Next.js "Failed to compile" with "Type error:" underneath.
The dev server transpiles TypeScript without checking types, so a type error is just a red squiggle you may never have seen. The production build runs the real type check and refuses to ship. Vibe-coded projects accumulate these because each AI edit is locally plausible but the types drift across files.
Fix: run npm run build locally, fix each reported error, repeat until it passes. The errors come with exact file and line numbers, so paste them one at a time into your AI tool if you want help; it fixes named type errors much more reliably than "my deploy is broken".
You will find advice online to silence the check (ignoreBuildErrors: true in Next.js, or stripping tsc from the build script). That trades a build failure for runtime bugs your users find instead of the compiler. Fix the errors.
Do not do this
Do not click "Retry deploy" repeatedly, and do not delete the site and recreate it. The build machine is deterministic: same repo, same result. And do not paste only the last line of the log into an AI chat, because the generic exit-code line sends it guessing in circles. The first real error, plus the clean-clone reproduction, solves this class of problem in one pass.
Still stuck?
Run the instant diagnosis. Paste your public repo URL into the form and an automated clean-room check does exactly the reproduction described above: fresh clone, clean install, production build, then a plain English report naming the exact blocker, in minutes, free. "Works locally, fails on deploy" is precisely the gap it exists to catch. Private repo or zip? Email works too.
Get an instant free diagnosisOr email me instead. Fixes with 24 hour turnaround start at $95. Prefer self-serve? The $5 instant diagnosis on Apify checks your repo privately, no public issue needed.