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.
- Run PostgreSQL with
-p 15840:5432, thendocker portandnetstat -ano | findstr :15840: the listener is Docker Desktop's backend, not postgres - Run it again with
-p 127.0.0.1:15841:5432and compare the listeners netsh interface ipv4 show excludedportrange protocol=tcp, then publish on a port inside one of the ranges and read the error- Start the API with
ASPNETCORE_URLS=http://127.0.0.1:8080inside the container and curl it from Windows; then withhttp://+:8080 docker network create, start the database and the API on it, and call/patientsthrough the service name- Repeat without the network and read the Npgsql exception in
docker logs - 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.