Three of the most read Fixes posts on this site are about Docker ports on Windows, and all three come from the same misunderstanding: who binds what, where. This part publishes a port and finds the Windows process that owns it, hits a port Windows has reserved, shows why an app that listens on localhost inside a container is unreachable, and connects two containers by name. The ClinicApi sample and a PostgreSQL 18 container do the work; every output is quoted.

Steps in this part
  1. Run PostgreSQL with -p 15840:5432, then docker port and netstat -ano | findstr :15840: the listener is Docker Desktop's backend, not postgres
  2. Run it again with -p 127.0.0.1:15841:5432 and compare the listeners
  3. netsh interface ipv4 show excludedportrange protocol=tcp, then publish on a port inside one of the ranges and read the error
  4. Start the API with ASPNETCORE_URLS=http://127.0.0.1:8080 inside the container and curl it from Windows; then with http://+:8080
  5. docker network create, start the database and the API on it, and call /patients through the service name
  6. Repeat without the network and read the Npgsql exception in docker logs
  7. From a container, pg_isready -h host.docker.internal -p 15840

Who owns a published port

With the database published as -p 15840:5432, docker port answers for both address families:

5432/tcp -> 0.0.0.0:15840
5432/tcp -> [::]:15840

On Windows, three sockets are listening, and none of them belongs to postgres:

  TCP    0.0.0.0:15840          0.0.0.0:0              LISTENING       35452
  TCP    [::]:15840             [::]:0                 LISTENING       35452
  TCP    [::1]:15840            [::]:0                 LISTENING       8752

Process 35452 is com.docker.backend.exe, Docker Desktop's backend. The container's postgres listens inside the Linux VM from Part 1; on Windows the backend acts as the proxy, and the documentation says so: "Docker Desktop's backend process listens on the specified host port". The third socket belongs to wslrelay.exe; the run did not establish why, so take it as observed. What matters is the first line: with no address in -p, the port is open on every interface of your PC. The engine documentation is blunt about it: "Publishing container ports is insecure by default." Add the loopback address and only this machine can reach it:

  TCP    127.0.0.1:15841        0.0.0.0:0              LISTENING       35452

One tool note: Get-NetTCPConnection did not list the 0.0.0.0 listener at all on this PC while netstat did, so use netstat -ano for this check.

Ports Windows has reserved

A port can look free and still fail. Windows keeps excluded ranges, most of them reserved by Hyper-V for WSL and Docker themselves, and they change: on this PC the list had ten ranges, port 80 among them, and a new range appeared within a minute of the first listing. Publishing on 57250, inside the range 57219 to 57318, failed in 0.3 seconds although netstat showed nothing on it:

docker: Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:57250 -> 127.0.0.1:0: listen tcp 0.0.0.0:57250: bind: An attempt was made to access a socket in a way forbidden by its access permissions.

The dynamic range on this PC starts at 49152 with 16384 ports, and every reserved range sits inside it, so a port in the 15000s is a safer habit than one in the 50000s. The Fixes post on this message shows how to move a range. Compare the message with a port that Docker itself already holds, a second container on 15840:

docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint dw-p4-db-dup (9bd610074950eda889b4167b6c4c6d8aee24ab82066016a6d9a61f278cc34c82): Bind for 0.0.0.0:15840 failed: port is already allocated

Windows refused the first; Docker's own bookkeeping refused the second, and it left the container behind in the Created state, so the next attempt with the same name hits the name conflict from Part 2. The allocated-port post covers finding the other container.

Inside a container, localhost is the container

The API image was started with the app bound to loopback, ASPNETCORE_URLS=http://127.0.0.1:8080, and published on 15842. The log said Now listening on: http://127.0.0.1:8080, and docker port happily showed the mapping, because publishing does not know what the app binds. From Windows:

curl: (52) Empty reply from server

Not refused, not reset: the backend accepted the connection on Windows, the forward into the container found nobody listening on the container's outside address, and the connection closed with no bytes. The same image with http://+:8080 on 15843 answered 200 OK with the container's own hostname:

{"app":"ClinicApi","machine":"8f6ac7ac5033"}

The ASP.NET Core images already default to all interfaces, through ASPNETCORE_HTTP_PORTS=8080 in the base image; the trouble arrives when a launch profile, appsettings or an environment variable says localhost. A container that shares the first one's network namespace could still reach it on 127.0.0.1, which proves the app was alive; a container on the bridge going to the container's IP got Connection refused.

Two containers that find each other by name

On a user-defined network, names resolve. The database and the API were started with --network dw-p4-net, the API's connection string said Host=dw-p4-db2, and /patients through the published port returned the clinic's three patients:

[{"id":1,"fullName":"Maria Garcia","phone":"+00-555-0101"},{"id":2,"fullName":"David Chen","phone":"+00-555-0102"},{"id":3,"fullName":"Aisha Khan","phone":"+00-555-0103"}]

Inside the API container the name is a DNS answer, not a hosts entry:

172.22.0.2      dw-p4-db2

/etc/resolv.conf points at 127.0.0.11, Docker's embedded DNS server, which the documentation describes: containers "that attach to a custom network use Docker's embedded DNS server", while containers on the default bridge "cannot refer to each other by name". The same two containers without --network proved it. / still answered, /patients took four seconds and returned a 500 with an empty body, and the log held the real reason:

Npgsql.NpgsqlException (0x80004005): Name or service not known

That exception is the signature of a missing network, and Part 6 is where Compose creates the network for you so that you stop meeting it.

Reaching Windows from a container

The reverse direction has a name of its own. host.docker.internal resolved to 192.168.65.254 from a container, and pg_isready against it reached the database published on 15840 through the host, and the native PostgreSQL on 5432 that this PC runs outside Docker. The same probe against localhost from a container reported no response, for the reason of the previous section. The documentation's table says the name "Resolves to the internal IP address of your host"; use it in a connection string when the database runs on Windows and the app in a container, and never the other way round.

Two leftovers the run found: a docker run --rm that fails to start, and a plain docker rm of a container whose image declares a volume, both leave an anonymous volume behind. docker rm -v does not. Part 8 lists what else accumulates.

Frequently asked

Why can I not reach my app in a Docker container on localhost?
Usually because the app inside the container binds 127.0.0.1, which is the container's own loopback, not the host's. Docker publishes the port anyway and curl from Windows gets an empty reply. Bind the app to all interfaces inside the container, http://+:8080 for ASP.NET Core, and publish the port with -p; the .NET images already do this through ASPNETCORE_HTTP_PORTS unless a URL setting overrides it.
Why does Docker say ports are not available on Windows when the port is free?
Windows reserves ranges of ports for Hyper-V, WSL and Docker itself, and a port inside such a range cannot be bound even though netstat shows nothing on it. List them with netsh interface ipv4 show excludedportrange protocol=tcp; the ranges change over time. Pick a port outside the dynamic range, for example in the 15000s, or move the reservation.
How do two Docker containers talk to each other by name?
Put them on the same user-defined network: docker network create mynet, then --network mynet on both. Docker's embedded DNS at 127.0.0.11 resolves container names there, and the connection string uses the container name and the container port, not the published host port. On the default bridge names do not resolve and Npgsql reports Name or service not known.

Next: Part 5, PostgreSQL in a container for a .NET app: the environment variables, the init scripts, the volume that keeps your data, and the connection strings from the host and from another container.