Ports, listening vs connected sockets, the 4-tuple, binding to localhost vs all interfaces, container port mapping, and tools to see who is listening.
A port is a 16-bit number (0 to 65535) that lets one IP address host many services. A socket is the OS object for one endpoint: a listening socket bound to IP:port, and a connected socket for each accepted connection, identified by the 4-tuple (client IP, client port, server IP, server port).
The Docker problem is almost always binding: the app listens on localhost (127.0.0.1) inside the container, which is unreachable from outside its network namespace, or it listens on a different port from the one you mapped. The official .NET 10 images set ASPNETCORE_HTTP_PORTS=8080, which binds all interfaces on 8080 (the default since .NET 8), so map it with -p 5000:8080 and do not override it with a localhost URL.
| Range | Name | Notes |
|---|---|---|
| 0–1023 | Well-known | 80 HTTP, 443 HTTPS, 22 SSH, 53 DNS. Binding needs root or CAP_NET_BIND_SERVICE on Linux |
| 1024–49151 | Registered | 1433 SQL Server, 5432 PostgreSQL, 6379 Redis, 5672 RabbitMQ |
| 49152–65535 | Dynamic/ephemeral (IANA, Windows) | Linux uses 32768–60999 by default for outbound connections |
The .NET 8+ container images listen on 8080 by default (not 80) so they can run as a non-root user.
Server process Clients
listening socket 0.0.0.0:8080
│ accept()
├── connected socket (10.0.0.5:8080 ↔ 10.0.0.9:51544) ◄── client A
├── connected socket (10.0.0.5:8080 ↔ 10.0.0.9:51545) ◄── client A, 2nd connection
└── connected socket (10.0.0.5:8080 ↔ 10.0.0.7:40112) ◄── client B
One listening port serves thousands of connections, because each connection is unique by its 4-tuple.
host :5000 ──(docker -p 5000:8080)──► container eth0 172.17.0.2:8080
│
app bound to 127.0.0.1:8080 ✘ only reachable from inside the container
app bound to 0.0.0.0:8080 ✔ reachable through the mapping
Common causes:
launchSettings.json sets applicationUrl: http://localhost:5123. It is not used in containers, but people copy it into ASPNETCORE_URLS.-p 5000:80 while the app listens on 8080.docker logs).A team's container worked with docker run but failed health checks in Kubernetes. The Dockerfile set ASPNETCORE_URLS=http://localhost:8080, and the Kubernetes probe hit the pod IP, so it got "connection refused". kubectl exec + ss -ltnp showed 127.0.0.1:8080. Changing to http://+:8080 (or removing the variable and relying on ASPNETCORE_HTTP_PORTS) fixed it.
Checks practical understanding of ports, sockets and container networking, which trip up many developers.
Green flags
ss/netstat.Red flags
socket() ─► bind(0.0.0.0:8080) ─► listen(backlog) ─► accept() ─► read/write ─► close()
│
kernel completes TCP handshakes and
queues them; accept() takes the next one
If the accept queue (backlog) is full, new connections are dropped or reset under load. Kestrel accepts quickly, but a starved thread pool can still delay it.
| Bind to | Reachable from |
|---|---|
127.0.0.1 / localhost |
Same machine (same container / network namespace) only |
0.0.0.0 / + / * |
All IPv4 interfaces |
[::] |
All IPv6 (and IPv4 if dual-mode) |
A specific IP, e.g. 10.0.0.5 |
That interface only |
Each container has its own network namespace, so its localhost is not the host's localhost. From inside a container, the host is host.docker.internal (Docker Desktop) or the bridge gateway IP.
builder.WebHost.ConfigureKestrel(o => o.ListenAnyIP(8080)) or app.Urls.Add.Kestrel:Endpoints in configuration.ASPNETCORE_URLS / --urls.ASPNETCORE_HTTP_PORTS / ASPNETCORE_HTTPS_PORTS (.NET 8+), which bind to all interfaces.http://localhost:5000 outside containers.Kestrel logs Now listening on: http://[::]:8080 at startup; read it.
| Error | Meaning |
|---|---|
| Connection refused (RST) | Reached the host, nothing listening on that IP:port (or bound to loopback only) |
| Timeout | Packets dropped: firewall, wrong IP, routing, or host down |
| Address already in use | Another process (or a previous instance) holds the port |
| Permission denied on bind | Port < 1024 as non-root on Linux |
ss -ltnp # Linux: listening TCP sockets with process
netstat -ano | findstr :8080 # Windows: who listens on 8080 (PID)
lsof -i :8080 # macOS/Linux
docker port <container> # current mappings
nc -vz localhost 5000 # can I connect?
docker run --rm -p 5000:8080 -e ASPNETCORE_URLS=http://localhost:8080 <your image> and curl localhost:5000: connection reset or refused.http://+:8080, rerun and curl again.ss -ltn in both cases to see 127.0.0.1:8080 vs *:8080.// Explicit, container-friendly binding
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenAnyIP(8080); // 0.0.0.0 and [::]
// options.ListenLocalhost(9090); // admin endpoint only reachable locally
});
var app = builder.Build();
app.MapGet("/healthz", () => Results.Ok("ok"));
app.Run();
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
USER $APP_UID
# .NET 8+ images already set ASPNETCORE_HTTP_PORTS=8080 (binds all interfaces)
EXPOSE 8080
ENTRYPOINT ["dotnet", "Orders.Api.dll"]
docker run --rm -p 5000:8080 orders-api:dev # host 5000 → container 8080
curl -i http://localhost:5000/healthz
# Inside a running container or pod
ss -ltnp # expect *:8080 or [::]:8080, not 127.0.0.1:8080
Mistake: Binding to localhost inside a container.
Why it is wrong: The container's loopback is not reachable through Docker's port mapping or from Kubernetes probes, which connect to the pod IP.
Instead: Bind to all interfaces (+, 0.0.0.0, ListenAnyIP) or rely on the image's ASPNETCORE_HTTP_PORTS.
Mistake: Mapping the wrong container port. Why it is wrong: Older guides map port 80, but current .NET images listen on 8080, so the mapping points at nothing. Instead: Read Kestrel's "Now listening on" log and map to that port.
Mistake: Copying launchSettings.json URLs into the Dockerfile.
Why it is wrong: Those URLs use localhost and developer ports meant for dotnet run, not for containers.
Instead: Leave container binding to ASPNETCORE_HTTP_PORTS or an explicit ListenAnyIP in code.
Mistake: Calling another container at localhost.
Why it is wrong: localhost is the calling container itself, so the request is refused.
Instead: Use the Docker Compose service name or the Kubernetes Service DNS name (for example http://orders-db:5432 or orders.default.svc.cluster.local).
Mistake: Treating "connection refused" and "timeout" as the same. Why it is wrong: They point to different layers: refused is the app's binding, a timeout is usually a firewall or routing problem. Instead: Use the exact error to decide where to look next.
Most "it works locally but not in Docker or Kubernetes" bugs are network namespace bugs: each container has its own loopback, interfaces, ports and DNS view.
| Symptom | Likely cause | Check |
|---|---|---|
| Refused on mapped port | App bound to 127.0.0.1, or wrong port | ss -ltnp in the container, Kestrel log |
| Refused on probe in Kubernetes | Same, or probe port differs from container port | Pod spec containerPort and probe port |
| Timeout | NetworkPolicy, security group, wrong IP | nc -vz from a debug pod |
| Name not resolved | Wrong service name or namespace | nslookup service.namespace.svc.cluster.local |
Works with --network host only |
Binding or port mapping problem hidden by host networking | Re-test with default bridge networking |
I make binding explicit in one place, log it at startup, and keep internal endpoints (metrics, admin) on a separate port that is not exposed through the Service or ingress, so exposure is a deliberate decision. A CI smoke test that runs the image with normal bridge networking and curls the health endpoint catches these bugs before deployment.
A firewall allows or blocks traffic based on rules (source, destination, port, protocol, and for L7 firewalls/WAFs, HTTP content).
Troubleshoot by symptom: timeout usually means dropped by a firewall or routing, refused means reached the host but nothing listening, reset after idle often means a firewall or load balancer idle timeout removed the connection state. Test from the source (nc -vz, Test-NetConnection), check rules and flow logs in both directions, and set TCP keep-alives or shorter pool idle timeouts below the firewall's idle timeout.
Two different limits produce similar errors:
EMFILE, "Too many open files"): the process hit its ulimit -n. Something is leaking files or sockets.EADDRNOTAVAIL, "Cannot assign requested address"): too many outbound connections to the same destination, usually closed sockets stuck in TIME_WAIT for 60 s.The classic .NET cause is new HttpClient() per request. Fix it with a shared HttpClient using SocketsHttpHandler { PooledConnectionLifetime = ... }, or IHttpClientFactory. Diagnose with ls /proc/<pid>/fd | wc -l, lsof -p, ss -s and ss -tan state time-wait.
An IP address identifies a network interface so packets can be routed to it.
192.168.1.10. About 4.3 billion addresses, which ran out, so most networks use private ranges behind NAT.2001:db8::1. Enough addresses for everything to be globally routable.10.0.1.0/24 means the first 24 bits are the network, leaving 8 bits (256 addresses) for hosts.10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Loopback: 127.0.0.1, ::1.How to tell it (STAR)
**S:** Our [orders API] would not become ready in [AKS], although it ran fine on my laptop. **T:** I had to get the migration back on schedule. **A:** Inside the pod, `ss` showed the app listening on `127.0.0.1:8080`, so probes from the kubelet were refused. The Dockerfile had a `localhost` URL copied from launch settings, and host networking had hidden it locally. I removed it and used the image's default port binding. **R:** Pods went ready in minutes, and a CI smoke test now catches binding mistakes before deployment.
Last updated 23 Oct 2026.