How to Bind a Node App to 0.0.0.0 (Fix nginx 502 Bad Gateway)

nginx returns 502 Bad Gateway and its error log says "connection refused" even though your Node app starts cleanly. The cause is almost always the bind address: the app is listening on 127.0.0.1:3000 (loopback only). Change it to 0.0.0.0 so nginx can connect, and the 502 disappears.

DevOps Engineernodejsnginxnetworking

Why nginx gets "connection refused"

A reverse proxy and the app it proxies to are separate processes reaching each other over a network interface. When your Node app calls:

server.listen(3000, '127.0.0.1');

it binds to the loopback interface only. 127.0.0.1:3000 is reachable from the same host context, but not from every interface. When nginx (or a sidecar, or a container bridge) tries to open a connection to the upstream, the kernel refuses it because nothing is listening on the interface nginx used. nginx surfaces that refusal as 502 Bad Gateway and logs connect() failed (111: Connection refused) while connecting to upstream.

The app "starts without errors" the whole time, which is why this wastes 20 minutes if you do not check the bind address first.

Step 1: confirm the bind address

Look at what is actually listening, not just whether the process is up:

ss -tlnp | grep :3000

A loopback-only bind looks like this - note the 127.0.0.1:

LISTEN 0 511 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=1842,fd=18))

That single 127.0.0.1 is the whole bug. A correct bind shows 0.0.0.0:3000.

You can prove it further: curl http://localhost:3000/ works (loopback), but the request through curl http://localhost:80/ returns 502 because nginx cannot reach the upstream.

Step 2: bind to all interfaces

Change the listen call in server.js to bind on 0.0.0.0:

const PORT = 3000;
server.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});

Dropping the host argument entirely (server.listen(PORT)) also works, because Node defaults to all interfaces. Being explicit about 0.0.0.0 documents the intent.

Step 3: restart and verify

The process must be restarted for the new bind to take effect:

pkill -f "node server.js"
node server.js > /var/log/node/app.log 2>&1 &

Confirm the new bind, then test through nginx:

ss -tlnp | grep :3000            # now shows 0.0.0.0:3000
curl -s -o /dev/null -w "%{http_code}" http://localhost:80/

The status code is now 200 instead of 502. If nginx still complains, reload it with nginx -s reload so it re-resolves the upstream, and re-check /var/log/nginx/error.log.

127.0.0.1 vs 0.0.0.0

This is a top-five deploy-time gotcha for containerized services: the fix is one argument, but you only find it fast if you check ss/netstat before assuming the app is broken.

Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.

Try it in a workspace →

What you'll practice

FAQ

Why does nginx return 502 Bad Gateway when my Node app is running?

nginx returns 502 when it cannot reach the upstream it proxies to. The most common cause is the app listening on 127.0.0.1 (loopback only), so nginx gets a connection refused. Check ss -tlnp for the bind address and switch the app to 0.0.0.0.

What is the difference between 127.0.0.1 and 0.0.0.0 for a listening server?

127.0.0.1 binds to the loopback interface only, so just the local host can connect. 0.0.0.0 binds to every network interface, so a reverse proxy, sidecar, or container bridge on a different interface can reach the app. Behind nginx you almost always want 0.0.0.0.

How do I make a Node.js app listen on all interfaces?

Pass 0.0.0.0 as the host argument: server.listen(3000, '0.0.0.0'). Omitting the host argument entirely also works because Node defaults to all interfaces. Restart the process afterward so the new bind takes effect.

How do I check what address a process is listening on in Linux?

Run ss -tlnp (or netstat -tlnp) and look at the address before the colon. 127.0.0.1:3000 means loopback only; 0.0.0.0:3000 means all interfaces. The users field shows the process name and PID bound to that port.

Keep learning

Fix EADDRINUSE: Port Already in UseDevOps projectFix a CORS Error in the BrowserDevOps projectExpose a Kubernetes DeploymentDevOps projectDevOps roadmapStep by step to hiredDevOps interview questionsSTAR answersAll DevOps projectsProjects hub

Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.

Open the workspace →