Why Does My App Work Locally but Not in Production?
The app runs perfectly on your machine and breaks the moment it's deployed. It's almost always one of about a dozen causes — missing environment variables, localhost URLs, a filesystem that doesn't persist. How to find which one, in the order most likely to be it.
It worked on your machine. You deployed it. Now it shows a blank page, a 500 error, or a login screen that loops forever — and nothing in the code changed.
This is the most common problem in deployment, and it's especially common with apps built by AI tools, because the AI tested everything against your computer. The good news is that the causes are few and well known. The code is almost never the problem. The difference is the environment the code runs in.
Here are the usual suspects, roughly in order of how often each one is the answer.
First: read the logs
Before guessing, look. Every host shows two kinds of logs, and you need both:
- Build logs — what happened when the host installed your dependencies and built the app. A failure here means the app never started.
- Runtime logs — what the running server printed. A crash, a database connection error, or an unhandled exception shows up here.
And one more place people forget: the browser's developer console (right-click → Inspect → Console, then the Network tab). If the page loads but data doesn't, the console usually says exactly which request failed and why.
If you're using an AI assistant to debug, paste it the actual error text. "It doesn't work in production" gets you guesses; a stack trace gets you a fix.
1. Environment variables are missing
This is the answer more often than everything else combined.
Your local app reads secrets and settings from a .env file — the database URL, API keys, the Stripe secret. That file is (correctly) not committed to git, which means it did not get deployed. The production server has no idea what DATABASE_URL is.
Fix: open your host's environment variable settings and add every variable your app uses. Open your local .env and go down it line by line. Then redeploy — most hosts only read environment variables at build or start time.
Also check: that production values are production values. Copying your local DATABASE_URL points production at your laptop's database, which the server can't reach.
2. Frontend variables were baked in at build time
A subtler version of the same problem. In frontend frameworks, variables prefixed with VITE_, NEXT_PUBLIC_, or REACT_APP_ aren't read when the page loads — they're copied into the JavaScript at build time.
So if you add or change one in your host's settings after the build, the running app still has the old value, or no value at all. You need to rebuild, not just restart.
(And since those values end up in the browser, anything with that prefix is public. Secrets never go there — see How to Keep API Keys Out of an AI-Built App.)
3. Something still points at localhost
Search your code for localhost and 127.0.0.1. AI tools love to hardcode them:
fetch('http://localhost:3000/api/orders')
On your machine, that reaches your server. In production, it tells the visitor's browser to connect to the visitor's own computer, which fails. Use a relative path (/api/orders) or an environment variable for the API's base URL.
The same applies to anything that calls back to you: OAuth redirect URLs ("Sign in with Google" configured for localhost:3000), payment webhooks, and email links. Each of those is configured in a third-party dashboard, and each needs the production URL added.
4. The server listens on the wrong address or port
Locally, your server might listen on port 3000 on 127.0.0.1. Most hosts tell your app which port to use through a PORT environment variable and expect it to listen on all interfaces, 0.0.0.0.
const port = process.env.PORT || 3000
app.listen(port, '0.0.0.0')
If the logs say the server started but the host reports it as unhealthy or times out, this is the likely cause.
5. The database isn't the one you think
Several variations:
- The local app used SQLite — a database stored as a single file. On many hosts the filesystem is wiped on every deploy, so the database resets. Or the file never existed in production at all.
- Migrations never ran. Your local database has the tables; the production database is empty. The app crashes the first time it queries a table that doesn't exist. Run your migration command against production as part of deployment.
- The connection needs SSL. Managed Postgres providers usually require encrypted connections, and some database drivers need
?sslmode=requireor an SSL option set explicitly. - Too many connections. Serverless hosts can open a new database connection per request and exhaust the database's limit under load. Use connection pooling.
We cover picking a production database in Which Database Should an AI-Built App Use?.
6. Uploaded files disappear
User uploads a profile photo, it works, and the next day it's gone. Many hosts give your app a filesystem that is temporary: it's reset on every deploy or restart, and on serverless platforms each request may land on a different machine entirely.
Files that must persist belong in persistent storage — object storage like S3, or a host that mounts a persistent disk. Never on the app server's local disk unless you know it survives.
7. Filenames differ only by capitalisation
Mac and Windows treat Logo.png and logo.png as the same file. Linux — which almost every server runs — does not. If your code imports ./components/Header and the file is header.tsx, it works locally and fails the build in production.
The build log will say "module not found." Check the exact capitalisation of the path it names.
8. Dev mode hid the problem
Running npm run dev isn't the same as the production build. Dev servers are forgiving: they skip type errors, compile on demand, and proxy API requests for you. The production build is strict and does none of that.
Run the production build locally before deploying:
npm run build
npm start
If it breaks locally now, you've reproduced the production bug on your own machine, which makes it far easier to fix.
9. Different versions of everything
Your laptop has Node 22; the host defaults to Node 18. A package uses a feature that doesn't exist in the older version, and the app crashes on startup.
Pin the version. For Node, set "engines": { "node": "22.x" } in package.json or add a .nvmrc file; for Python, a .python-version or your host's runtime setting. Also make sure the lockfile (package-lock.json, pnpm-lock.yaml) is committed, so production installs the same package versions you tested.
10. A dependency was only installed on your machine
Something you installed globally, or a package that ended up in devDependencies when it's actually needed at runtime. Some hosts don't install dev dependencies for production. If the logs say "cannot find module X," check where X is listed in package.json.
11. Cookies and logins break on HTTPS
Logins that work locally but loop in production are usually cookie settings. Production runs on HTTPS, and browsers enforce rules there that don't apply on localhost: cookies may need Secure, cross-site setups need SameSite=None, and a cookie scoped to the wrong domain is silently dropped.
If your frontend and backend are on different domains, you'll also hit CORS errors — the browser refusing to let one domain read responses from another. The console names the blocked request. The fix is on the backend: allow your production frontend's exact origin.
12. Time zones
Your laptop is in your time zone. Servers almost always run in UTC. "Today's orders," scheduled emails, and date comparisons can be off by hours. Store times in UTC and convert only for display.
The pattern behind all of these
Look back at the list. None of these are bugs in your logic. They're all differences between two environments: the one you tested in and the one you shipped to. That's why "it works on my machine" is so persistent — your machine is a different environment from production in a dozen invisible ways.
There are two ways to shrink the gap:
- Make your local setup more like production. Run the production build locally, use Postgres locally rather than SQLite, keep an
.env.examplelisting every variable the app needs. - Develop in an environment that already is production-shaped. When the code, the database, and the running app all live in the same kind of environment they'll be deployed to, most of this list can't happen in the first place.
A quick checklist
- Read the build logs, runtime logs, and browser console
- Every environment variable set in the host, with production values
- Rebuilt after changing any
VITE_/NEXT_PUBLIC_variable - No
localhostleft in code, OAuth settings, or webhooks - Server listens on
process.env.PORTand0.0.0.0 - Database migrations run against production
- Uploads go to persistent storage
-
npm run build && npm startworks locally - Runtime version pinned, lockfile committed
EasySpawn gives each project a persistent workspace where the app, its managed Postgres, and its environment variables live together — so Claude Code builds and tests against the same kind of environment the app runs in, instead of discovering the differences after deploy. See how it works for AI-built apps or join the waitlist.
Related: You Built an App With AI. Now What? · How to Connect a Custom Domain to Your App · What Is Localhost? · CORS Errors Explained · File Paths Explained · What Is an IP Address and a Port?
Keep reading
What Is Web Hosting? A Plain-English Guide for First-Time App Builders
Your app has to run on a computer that's always on and connected to the internet. That's hosting. The main kinds — static hosting, app hosting, servers you manage, and managed platforms — what each is for, and how to tell which one your app needs.
What Is Next.js? A Beginner's Guide
Next.js is the React framework that v0 and many AI tools produce by default. What it adds on top of React, how file-based routing works, server vs client components, API routes, what 'rendering' means, and what you need to know to deploy it.