deployment
Why your app works locally but not on the server
A plain-English guide to why an app works locally not in production, and how to find the missing assumption.
Your app runs perfectly on your laptop, then falls over the moment real users reach the server. That is frustrating because it feels like the same code suddenly changed its mind.
Short version: An app works locally but not on the server because production is a different kitchen: different environment variables, network rules, file paths, permissions, software versions, and public traffic. The fix is to compare local and server assumptions one by one, then check logs, port binding, database access, domain and HTTPS, and background services. The error is usually not “the server is broken”; it is one missing assumption your laptop was quietly providing.
Why does “it works on my machine” happen?
Your laptop is a forgiving place. It has your login session, your saved secrets, your development database, your folders, your tools, and often a browser already pointed at the right local address.
A server is less forgiving. It starts your app without your personal desktop wrapped around it. It only knows what you explicitly gave it.
That is why the classic problem behind “app works locally not in production” is not usually mysterious. Local development is like cooking in your own kitchen, where you know which drawer has the knife. Production is like cooking in a rented kitchen. Same recipe, different drawers, different oven, different gas switch.
The code can be fine and still fail because the surroundings changed.
What changes when the app reaches the server?
The first difference is environment variables. These are values your app reads from the outside world, such as database passwords, API keys, app mode, email settings, or secret keys. Locally, they may live in a hidden file or your shell. On the server, they must exist where the app actually runs.
The second difference is network access. On your laptop, the database might be at localhost, which means “this same machine.” On the server, localhost means the server itself, not your laptop and not a separate database service. This is how you get errors like ECONNREFUSED, connection refused, or could not connect to server.
The third difference is file paths and permissions. Your app may write uploads, cache files, or logs into a folder that exists locally but not on the server. Or the folder exists, but the app is not allowed to write there. That often shows up as permission denied, ENOENT, or “no such file or directory.”
The fourth difference is versions. Your laptop may have one version of Node, Python, PHP, Ruby, or a system library, while the server has another. A tiny version mismatch can become module not found, a broken build, or a blank page.
If you are using containers, they reduce some of this drift, but they do not remove every production concern. Domains, certificates, ports, storage, and databases still need to line up. If Docker is part of your setup, this beginner guide may help: Docker on a server for beginners.
Why does the website load but still break?
Sometimes the domain works and the homepage appears, but the app fails when you log in, upload a file, send email, or call the API. That usually means the front door is open, but a room inside the house is locked.
A common example is 502 Bad Gateway. This often means the web server reached the public internet correctly, but it could not talk to your app behind the scenes. The domain and HTTPS may be fine, while the app process is stopped, listening on the wrong port, or crashing after startup.
Another example is mixed HTTP and HTTPS. Your site loads over HTTPS, but the browser blocks an API request because it points to an insecure http:// address. To the user, it looks like the app is broken. To the browser, it is refusing to carry a package through an unsafe side door. If this layer is confusing, read why a website is not loading for the domain, server, and app layers.
Background workers are another hidden failure. Your page may load, but emails never send, image processing never finishes, or scheduled jobs never run. Locally, you may have started those workers by hand without realizing they need their own long-running process on the server.
How do you troubleshoot without guessing?
Start by asking: what did my laptop provide that the server does not?
Check the app logs first. Logs are the app’s diary. They often say the real thing: missing secret, failed database connection, wrong file path, expired token, unavailable service, or failed migration.
Then separate the problem into layers. Can the domain reach the server? Can the web server reach the app? Can the app reach the database? Can the app write files? Can it reach outside services like email, payment, or storage?
This is better than changing ten things at once. If you change everything, you learn nothing. Move like a plumber checking one pipe at a time: water supply, valve, joint, drain.
Also check what happens after a restart. A local app often works because you manually started it. A production app must survive reboots, crashes, and deployments. If it only works until the next restart, the setup is not finished.
For a broader walkthrough of getting a small app online without turning deployment into a second job, see deploy a small web app without DevOps.
FAQ
Why does my app work locally but not in production? Because production has different environment variables, paths, permissions, network rules, service versions, and public traffic.
**Does 502 Bad Gateway mean my code is broken?** Not always. It often means the public web server cannot reach the app process behind it.
Why does the database work locally but fail on the server? The server may be using a different hostname, blocked port, missing password, or a database that was never migrated.
Can HTTPS break an otherwise working app? Yes. Wrong certificates, expired certificates, or insecure API URLs can make the browser block requests.
The shortcut
Server Manager helps by keeping the moving pieces visible instead of scattered across memory, notes, and half-remembered setup steps. The outcome is that your app has a legible path from domain to service, so failures like a wrong port, a missing certificate, or one project colliding with another are easier to spot.
It also helps months later, when the original setup is no longer fresh in your head. You are less likely to forget which app owns which domain, which service is supposed to be running, or why a project was arranged a certain way.
The real benefit is not that production becomes magic. It is that production stays understandable, so the next 502 Bad Gateway, missing environment value, or expired certificate feels like a solvable problem instead of a dark room.
How do you make the next deploy boring?
A healthy deploy feels boring because each assumption is named. The app knows its secrets, the database is reachable, files have a place to live, the domain points to the right server, HTTPS is valid, and the app keeps running after a restart.
That is the win: not just making the app work once, but knowing why it works. When local and production stop being two different worlds, you can ship changes with a lot less dread.