📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What are ports and sockets? Your ASP.NET Core API works on your laptop but nothing answers when you run it in Docker. What is going on?

Ports, listening vs connected sockets, the 4-tuple, binding to localhost vs all interfaces, container port mapping, and tools to see who is listening.

Mid-Level Debugging Intermediate docker kestrel networking ports sockets troubleshooting

🎯 Key points

  • A port lets one IP host many services; ports below 1024 need root or CAP_NET_BIND_SERVICE on Linux, which is why containers use 8080.
  • A listening socket is bound to IP:port; each accepted connection gets its own socket, unique by the 4-tuple, so one port serves thousands of connections.
  • Each container has its own network namespace, so localhost inside a container is the container's own loopback, not the host's.
  • Bind to all interfaces in containers (http://+:8080, 0.0.0.0, [::] or ListenAnyIP); the official .NET 10 aspnet images do this by default through ASPNETCORE_HTTP_PORTS=8080.
  • Kestrel endpoint precedence: code (ConfigureKestrel), then Kestrel:Endpoints configuration, then ASPNETCORE_URLS, then ASPNETCORE_HTTP_PORTS; launchSettings.json is only used by dotnet run and Visual Studio.
  • Connection refused means the host was reached but nothing listens on that IP:port; a timeout means packets were dropped (firewall, routing, wrong IP).
  • Other containers are reached by service or container name through Docker or Kubernetes DNS, never by localhost; host.docker.internal reaches the host on Docker Desktop.
  • Diagnose with Kestrel's 'Now listening on' log, ss -ltnp or netstat -ano, docker port, kubectl exec, and nc -vz or curl from inside and outside the container.

⏱ 30-second answer

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.

🗣 Full answer (senior)

Ports

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.

Sockets

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.

The Docker problem

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:

  1. launchSettings.json sets applicationUrl: http://localhost:5123. It is not used in containers, but people copy it into ASPNETCORE_URLS.
  2. Port mismatch: mapping -p 5000:80 while the app listens on 8080.
  3. HTTPS redirection to a port that is not mapped.
  4. The container exits on start (check docker logs).

Production example

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.

🔍 What the interviewer is checking

Checks practical understanding of ports, sockets and container networking, which trip up many developers.

Green flags

  • Explains listening vs connected sockets and the 4-tuple.
  • Immediately suspects binding to localhost inside the container.
  • Knows .NET 8+ default port 8080 and how to check with ss/netstat.
  • Distinguishes refused from timeout.

Red flags

  • Thinks each client needs a separate server port.
  • Suggests disabling the firewall without diagnosing.

📚 Learn in depth

1. The socket lifecycle (server side)

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.

2. Bind addresses

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.

3. Kestrel configuration precedence

  1. Code: builder.WebHost.ConfigureKestrel(o => o.ListenAnyIP(8080)) or app.Urls.Add.
  2. Kestrel:Endpoints in configuration.
  3. ASPNETCORE_URLS / --urls.
  4. ASPNETCORE_HTTP_PORTS / ASPNETCORE_HTTPS_PORTS (.NET 8+), which bind to all interfaces.
  5. Default http://localhost:5000 outside containers.

Kestrel logs Now listening on: http://[::]:8080 at startup; read it.

4. Errors and what they mean

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

5. Tools

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?

6. Try it yourself

  1. Run docker run --rm -p 5000:8080 -e ASPNETCORE_URLS=http://localhost:8080 <your image> and curl localhost:5000: connection reset or refused.
  2. Change to http://+:8080, rerun and curl again.
  3. Inside the container, run ss -ltn in both cases to see 127.0.0.1:8080 vs *:8080.

💻 Code

// 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

⚠ Common mistakes

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.

🏛 Senior insight: trade-offs

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.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says the connection times out rather than being refused Why: Moves to firewall and connectivity troubleshooting
    How to answer

    A firewall allows or blocks traffic based on rules (source, destination, port, protocol, and for L7 firewalls/WAFs, HTTP content).

    • Stateful firewalls (Azure NSGs, AWS security groups, most host firewalls) track connections, so return traffic is automatically allowed. Stateless filters (AWS NACLs) need rules both ways.
    • Layers: host firewall (iptables/nftables, Windows Defender Firewall), cloud NSG/security group, network appliances, Kubernetes NetworkPolicy, and WAF at the edge.

    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.

  2. When: Candidate mentions ephemeral ports Why: Probes port exhaustion and TIME_WAIT
    How to answer

    Two different limits produce similar errors:

    1. Descriptor exhaustion (EMFILE, "Too many open files"): the process hit its ulimit -n. Something is leaking files or sockets.
    2. Ephemeral port exhaustion (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.

  3. When: Candidate is unsure what 0.0.0.0 means Why: Checks IP addressing fundamentals
    How to answer

    An IP address identifies a network interface so packets can be routed to it.

    • IPv4: 32 bits, written 192.168.1.10. About 4.3 billion addresses, which ran out, so most networks use private ranges behind NAT.
    • IPv6: 128 bits, written 2001:db8::1. Enough addresses for everything to be globally routable.
    • CIDR 10.0.1.0/24 means the first 24 bits are the network, leaving 8 bits (256 addresses) for hosts.
    • Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Loopback: 127.0.0.1, ::1.
    • NAT rewrites private source addresses to a public one on the way out, so many machines share one public IP.

🛠 Real-world problems: how you handle them

Kubernetes probes fail because Kestrel listens on loopback
Situation
[Orders API] on .NET [10] moving from [App Service] to [AKS]. Worked with `docker run` on a laptop using `--network host`.
Signals
Pods in `CrashLoopBackOff`; readiness probe `connection refused` on port 8080; app logs showed it started fine.
Investigation
`kubectl exec` then `ss -ltnp` showed `127.0.0.1:8080`. The Dockerfile set `ASPNETCORE_URLS=http://localhost:8080`, copied from launchSettings. Host networking had hidden the problem locally.
Resolution
Removed the variable to use the image default `ASPNETCORE_HTTP_PORTS=8080`, which binds all interfaces. Probes passed and pods became ready.
Prevention
Container smoke test in CI that runs the image with normal bridge networking and curls the health endpoint; Dockerfile template for all services.

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.

↩ Usually asked after

More in Networking Basics

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me