Why `localhost` Works but Your API Doesn't
Learn why your API fails in production and how to fix common localhost vs deployed environment issues with practical steps.
The Problem: It Worked on My Machine
You've built a REST API. You run it locally, hit http://localhost:3000, and everything responds perfectly. You deploy to a server, and suddenly requests time out, return 403, or the database connection fails. This is the classic 'works on my machine' problem, and it's not magic. It's a set of concrete differences between your local environment and production.
In this article, I'll walk you through the most common reasons APIs fail after deployment and show you how to diagnose and fix each one. You'll leave with a systematic checklist to run through whenever your API works locally but not in the wild.
Before You Start
To follow along, you'll need a basic REST API (any language works, but I'll use Node.js/Express for examples), a server or cloud instance (like a VPS or a container), and SSH access to that server. You'll also want curl installed on your local machine and on the server for testing.
Make sure you can reproduce the issue: have your API running locally, then deployed, and observe the failure. The steps below will help you isolate the cause.
Step 1: Check the Listening Interface
The most common cause is that your server is listening only on localhost, not on all network interfaces. In development, you might start your app with app.listen(3000), which binds to 127.0.0.1 by default. In production, you need it to listen on 0.0.0.0 so it accepts connections from outside.
Here's how to check what your app is listening on:
Run this command on your server to see which interfaces your app is bound to:
ss -tlnp | grep node- If you see 127.0.0.1:3000, your app is only accessible locally.
- If you see 0.0.0.0:3000, it's listening on all interfaces.
- To fix it, set the HOST environment variable to 0.0.0.0 in your app:
Step 2: Update Your Server Code to Bind Correctly
In your Express app, you might have app.listen(3000). Change it to use the HOST environment variable, defaulting to 0.0.0.0 for production. Here's a minimal change:
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
app.listen(PORT, HOST, () => {
console.log(`Server running on http://${HOST}:${PORT}`);
});- In development, you can set HOST=127.0.0.1 to keep it local.
- In production, set HOST=0.0.0.0 in your environment or Dockerfile.
Step 3: Verify with curl from Outside
After updating, restart your app and test from your local machine (not the server) using curl. This simulates an external request:
curl -v http://YOUR_SERVER_IP:3000/health- If you get a response, the binding issue is fixed.
- If you get a connection timeout, move to the next step: firewall.
Step 4: Inspect and Configure the Firewall
Even if your app listens on 0.0.0.0, the server's firewall might block inbound traffic. On Ubuntu, ufw is common. Check the status and allow your port:
sudo ufw status
sudo ufw allow 3000/tcp- If ufw is inactive, you may not need to change it.
- For cloud providers, also check the security group / network firewall rules in their console.
Step 5: Check Reverse Proxy Configuration
In production, you often place Nginx or Apache in front of your Node.js app. If the proxy isn't configured correctly, requests can be misrouted or blocked. Here's a minimal Nginx config that forwards to your app:
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}- Make sure proxy_pass points to the correct address and port your app listens on.
- Check Nginx logs at /var/log/nginx/error.log for clues.
Step 6: Test with curl from the Server Itself
To isolate whether the problem is the app or the network, test locally on the server. If curl from the server works but from outside doesn't, it's a firewall or proxy issue. If it fails locally too, it's an app or binding issue.
curl -v http://localhost:3000/health
curl -v http://127.0.0.1:3000/health- If localhost works but the external IP fails, check firewall and proxy.
- If both fail, check the app's logs and binding.
Step 7: Validate Environment Variables
Often your API depends on environment variables like DATABASE_URL, JWT_SECRET, or API_KEYS. In production, these might be missing or wrong, causing the app to crash or behave differently. Compare your local .env with the server's environment:
env | grep -E 'DB|SECRET|API'- Ensure all required variables are set and match expected formats.
- Never hardcode secrets in your code; use environment variables.
Step 8: Check Database and External Service Connectivity
Your API might connect to a database or third-party service that is only reachable from your local network. In production, the database might be on a private network, require a different host, or have IP allowlists. Test the connection from the server:
nc -zv your-db-host 5432- If the connection times out, check network paths and security groups.
- Update the database host in your environment variable to the correct production endpoint.
Recommended Setup: A Production-Ready Start
To avoid these issues from the start, here's a minimal production setup that works: bind to 0.0.0.0, use environment variables, and run behind Nginx. I'd also add a health check endpoint. Here's a complete starter snippet:
git clone https://github.com/yourusername/your-api.git
cd your-api
npm install
# Set environment variables
export PORT=3000
export HOST=0.0.0.0
export DATABASE_URL=postgres://user:pass@db-host:5432/mydb
# Start the app
npm start- Use a process manager like PM2 to keep the app running.
- Set up Nginx as a reverse proxy and enable HTTPS with Let's Encrypt.
Troubleshooting Checklist
Run through this list in order when your API works locally but not in production:
- Check the listening interface with ss -tlnp and ensure it's 0.0.0.0.
- Test with curl from outside the server.
- Check firewall rules (ufw, iptables, cloud security groups).
- Verify reverse proxy configuration and logs.
- Test from the server itself to isolate network vs app.
- Compare environment variables between local and production.
- Test database and external service connectivity.
- Review application logs for errors.
FAQ
- Q: Why does my API work with localhost but not with my server's IP? A: The app is likely bound to 127.0.0.1. Change it to 0.0.0.0.
- Q: My firewall allows the port, but I still can't connect. What else? A: Check if your cloud provider has a separate security group or network ACL.
- Q: I'm using Docker. How do I bind to 0.0.0.0? A: In your app, set HOST=0.0.0.0, and in docker run use -p 3000:3000 to expose the port.
- Q: How do I debug if the API crashes on startup in production? A: Check logs (pm2 logs or journalctl), and ensure all environment variables are set.
Next Action
Now, go to your server, run ss -tlnp, and see what your app is listening on. If it's not 0.0.0.0, fix it and test again. That's the single most common fix for this problem.
Key takeaways
- Apply one concrete change from this post before collecting more reading.
- Prefer browser-side tools when the work involves secrets, tokens, or PII.
- Document the why next to the how so the next reviewer inherits context.
FAQ
- Who is this guide on api for?
- Working developers who need a practical take on why `localhost` works but your api doesn't — not a marketing overview. Skim the sections, apply one tip, then come back when you hit an edge case.
- Do I need an account to use the related tools?
- No. code.live tools run in your browser with no signup. Nothing you paste is uploaded to a server for the client-side utilities linked from this post.
- How often is this article updated?
- This post was published September 7, 2026. Fundamentals stay stable; check linked tool pages and official docs when version-specific behavior matters.