# ERR_CONNECTION_REFUSED: Localhost, Docker and Node Fixes

> Fix ERR_CONNECTION_REFUSED by checking listeners, localhost IPv4/IPv6, Docker port mappings, firewall rejection, service crashes and Kubernetes endpoints.

Source: https://howhttpworks.com/debug/err-connection-refused
Last reviewed: 2026-10-05

Error messages this page covers:
- `ERR_CONNECTION_REFUSED`
- `localhost refused to connect.`
- `curl: (7) Failed to connect to localhost port 3000 after 0 ms: Couldn't connect to server`
- `curl: (7) Failed to connect to localhost:3000 after 0 ms: Could not connect to server`
- `connect ECONNREFUSED 127.0.0.1:3000`
- `connect ECONNREFUSED ::1:3000`

> **TL;DR:** The TCP connection attempt was rejected before HTTP could start. Check the exact address and port with `curl -v`, then find the listener with `ss -ltnp` or `lsof`. For localhost, test IPv4 and IPv6 separately. For Docker, check both the app's bind address inside the container and the host-to-container port mapping. A running process is not proof that the port is listening.

## What it means

Chrome shows `ERR_CONNECTION_REFUSED`, with **localhost refused to connect.** when the hostname is localhost. [Chromium's error definitions](https://github.com/chromium/chromium/blob/main/net/base/net_error_list.h) distinguish a refused connection attempt from a reset connection. No HTTP response exists for the failed attempt, so changing request headers or CORS policy will not open the port.

The failure can appear in other clients as well. This curl output was captured against a closed local port with **curl 8.7.1 on macOS**:

```text
curl: (7) Failed to connect to localhost port 3000 after 0 ms: Couldn't connect to server
```

The wording changed. **curl 8.22.0**, the current release checked for this guide, formats the destination with a colon and uses “Could not” in its [connection failure source](https://github.com/curl/curl/blob/curl-8_22_0/lib/cf-ip-happy.c) and [error strings](https://github.com/curl/curl/blob/curl-8_22_0/lib/strerror.c). Illustrative output for the same direct connection:

```text
curl: (7) Failed to connect to localhost:3000 after 0 ms: Could not connect to server
```

Elapsed milliseconds vary. Exit code **7** means curl could not connect; use verbose output to identify the underlying failure rather than treating every code 7 as a refusal.

These Node messages were captured with **Node 26.10.0**, explicitly connecting to each loopback address on a closed port:

```text
connect ECONNREFUSED 127.0.0.1:3000
connect ECONNREFUSED ::1:3000
```

The address matters: `127.0.0.1` is IPv4 loopback; `::1` is IPv6 loopback. A listener on one is not a listener on the other.

## Refused, timeout and reset

- **Refused:** establishment gets an active rejection. A TCP SYN answered with RST is the usual closed-port case. [RFC 9293 Section 3.10.7.1](https://www.rfc-editor.org/rfc/rfc9293#section-3.10.7.1) specifies a reset response when no connection state exists for an incoming segment. A firewall can reject the attempt too.
- **Connection timeout:** establishment does not finish before the client's deadline. Silent packet drops, a missing return path or an unresponsive destination can cause this. A timeout later in an established connection is a separate diagnosis.
- **Reset:** the connection is aborted with a TCP RST, potentially during TLS or while HTTP data is moving. Follow [ERR_CONNECTION_RESET](https://howhttpworks.com/debug/err-connection-reset) if the connection was established and then broken.

An immediate failure is a useful clue, not a guarantee that the application itself rejected you. Identify the destination and the rejecting layer.

## Who sent it?

Run these on the machine where the client fails:

```bash
curl --noproxy '*' -v --connect-timeout 3 http://localhost:3000/
nc -vz 127.0.0.1 3000
nc -vz ::1 3000
```

Look at curl's `Trying` addresses and whether it ever prints `Connected to`. `--noproxy '*'` makes this a direct probe rather than a test through an environment-configured proxy. `nc -vz` checks the TCP port without making an HTTP request.

Then inspect the server, inside the relevant container or VM if necessary:

```bash
# Linux: TCP listeners, numeric addresses/ports and owning processes
sudo ss -ltnp
# macOS, or Linux with lsof installed
sudo lsof -iTCP -sTCP:LISTEN -nP
```

Match the port and bind address. `127.0.0.1:3000` accepts local IPv4 connections; `0.0.0.0:3000` listens on all local IPv4 interfaces. An IPv6 wildcard `:::3000` can also accept IPv4 on some systems, but that depends on the platform and socket configuration. Test both families.

## Fix it, in order of evidence

### 1. Nothing is listening on the requested port

If the listener list has no matching address and port, start the service and inspect its startup output. Check whether it selected a different port or failed before binding. Use the port the server actually opened, not the port assumed by the frontend configuration.

For a crash-looping service, inspect the failed process rather than repeatedly refreshing the browser:

```bash
# Replace my-app with the systemd unit
systemctl status my-app
journalctl -u my-app -n 100 --no-pager
# Containers, including those that exited
docker ps -a
docker logs --tail 100 my-app
# Kubernetes: previous container instance's logs
kubectl -n web describe pod POD_NAME
kubectl -n web logs POD_NAME --previous
```

A process that crashes before binding leaves no listener. During a restart loop, the listener can appear and disappear. Fix the startup exception, failed dependency or container termination shown in the logs.

### 2. The app is bound to loopback inside Docker or a VM

`localhost` refers to the machine or network namespace making the connection. Your browser's localhost is not the container's localhost, and a second container's localhost is not the first container.

For a bridge-networked container reached through a published port, bind the app to `0.0.0.0` inside the container. For example, a Node HTTP server:

```javascript
const http = require('node:http')
http.createServer((req, res) => res.end('ok\n')).listen(3000, '0.0.0.0')
```

Publish that container port on the host:

```bash
docker run --name my-app -p 127.0.0.1:8080:3000 my-app-image
curl --noproxy '*' -v http://127.0.0.1:8080/
```

Here the app listens on container port **3000** and the browser connects to host port **8080**. Binding the app to all container IPv4 interfaces is separate from deciding where Docker publishes the host port. [Docker's mapping documentation](https://docs.docker.com/engine/network/port-publishing/) confirms that the explicit host `127.0.0.1` binding keeps this mapping local to the host.

For a VM, bind to `0.0.0.0` or its reachable interface and connect to the guest's address, or configure the VM's NAT forwarding. Publishing a Docker port cannot fix an application listening only on container loopback.

### 3. The port mapping points at the wrong container port

```bash
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
```

Illustrative `PORTS` values:

```text
127.0.0.1:8080->3000/tcp
3000/tcp
```

The first publishes host 8080 to container 3000. The second lists a container port without a host mapping. `EXPOSE` alone does not publish a port. Compare the right side of the arrow with the listener inside the container; compare the left side with the URL you opened.

### 4. Localhost chose IPv6, but the server listens only on IPv4

Test each family and inspect Node's resolver output:

```bash
curl --noproxy '*' -4 -v http://localhost:3000/
curl --noproxy '*' -6 -v http://localhost:3000/
node -e 'require("node:dns").lookup("localhost", {all:true}, console.log)'
```

Node **17.0.0** changed the default lookup ordering to `verbatim`: keep resolver order instead of moving IPv4 first. If `::1` comes first and the client tries only that address, an IPv4-only server can fail. [`--dns-result-order=ipv4first`](https://nodejs.org/api/dns.html#dnssetdefaultresultorderorder) can diagnose that case:

```bash
node --dns-result-order=ipv4first client.cjs
```

[`autoSelectFamily`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) became enabled by default in **Node 20.0.0 and 18.18.0**. It tries resolved IPv6 and IPv4 addresses until one succeeds; if all fail, it emits an `AggregateError`. Explicit `family` or `localAddress` settings disable that selection. Check the client's options: DNS order alone does not establish the cause on current Node. A `dns.setDefaultResultOrder()` call also takes precedence over the CLI ordering flag.

Use an explicit loopback address for a client meant to use one family, or make the server listen on the required families. Retest both rather than assuming an IPv6 wildcard behaves identically on every OS.

### 5. A firewall rejects the attempt

Netfilter distinguishes [`reject` from `drop`](https://netfilter.org/projects/nftables/manpage.html). Reject sends an error response; `reject with tcp reset` can make an initial connection look refused. Drop silently discards the packet, which can leave the client waiting until a timeout. Other rejection types can produce different OS errors.

Compare a local probe on the server with one from the failing client. If the listener exists and the local probe works, inspect firewall rules and counters along the external path. Do not equate refusal with proof that no process exists; an intervening device can reject traffic to a healthy listener.

### 6. A Kubernetes Service has no usable endpoints

```bash
kubectl -n web get service my-app -o yaml
kubectl -n web get endpoints my-app
kubectl -n web get endpointslices -l kubernetes.io/service-name=my-app -o yaml
kubectl -n web get pods -o wide --show-labels
```

Check the Service selector against Pod labels, Pod readiness, and `targetPort` against the app's actual listener. `kubectl get endpoints` is useful on older clusters; use EndpointSlices as well, as the [current Kubernetes debugging guide](https://kubernetes.io/docs/tasks/debug/debug-application/debug-service/) does. Inspect endpoint readiness, not just whether an address is listed.

In [iptables kube-proxy](https://github.com/kubernetes/kubernetes/blob/master/pkg/proxy/iptables/proxier.go), no endpoints at all produces REJECT rules. No *local* endpoints under a `Local` traffic policy can instead produce DROP rules. Other cluster networking implementations can differ. Inspect endpoint state before inferring the cause from “refused” versus “timeout”.

## Reproduce and verify

Repeat the same client-side probe after the fix. First prove TCP connects, then prove the app answers:

```bash
nc -vz 127.0.0.1 3000
curl --noproxy '*' -v http://127.0.0.1:3000/
```

For Docker, use the published host port. For Kubernetes, probe the Service from the same network as the failing client and check its endpoints again. A TCP connection followed by an HTTP 404 or 500 proves refusal is resolved; the application response then needs its own diagnosis.

## Related

- [ERR_CONNECTION_RESET](https://howhttpworks.com/debug/err-connection-reset): the connection is aborted during TLS or data transfer.
- [nginx 502 Bad Gateway](https://howhttpworks.com/debug/nginx-502-bad-gateway): a reverse proxy cannot connect to its upstream.
- [ERR_CONNECTION_TIMED_OUT](https://howhttpworks.com/debug/err-connection-timed-out): no reply at all, from silent firewall drops, stale addresses, broken IPv6 or a full listen backlog.
- [DNS_PROBE_FINISHED_NXDOMAIN](https://howhttpworks.com/debug/dns-probe-finished-nxdomain): the name never resolved, so no connection was attempted.
