Ports
By the end of this lesson
Map container ports to the host and diagnose when it does not work.
A container has its own network namespace, which means its own set of port numbers. An API listening on port 8080 inside a container is not reachable on port 8080 of your machine, because those are two unrelated 8080s.
Publishing a port connects the two. You tell Docker that traffic arriving at a port on the host should be forwarded into a port inside the container. Nothing about the application changes — the forwarding happens outside it.
# Host 8080 forwards to container 8080
docker run -d --name employees-api -p 8080:8080 anvi/employees-api:1.4.0
# The numbers need not match: host 5001 forwards to container 8080
docker run -d --name employees-api-2 -p 5001:8080 anvi/employees-api:1.4.0
# Only your own machine can reach it, not the network you are on
docker run -d --name employees-api-3 -p 127.0.0.1:5002:8080 anvi/employees-api:1.4.0
# Let Docker pick free host ports for every port the image EXPOSEs
docker run -d --name employees-api-4 -P anvi/employees-api:1.4.0
# What ended up where
docker port employees-api-4
docker ps- -p takes host first, then container. The order is the one thing to memorise here.
- The second example is how several containers from one image coexist. The port inside is 8080 in every case; only the host side has to be unique.
- Adding 127.0.0.1 in front limits which host interface listens. Without it, the mapping is reachable by anything that can reach your machine — worth a thought on a shared or public network.
- -P publishes every EXPOSEd port on an arbitrary high host port. Convenient for tests that ask Docker where the port went, unhelpful when you need a predictable address.
- docker port prints one container's mappings. docker ps shows them in the PORTS column, in the form 0.0.0.0:8080->8080/tcp.
Publishing is only half of it. A server also chooses which address to listen on, and that choice is made inside the container.
Binding to 127.0.0.1, usually written as localhost, means "accept connections that originate on this machine". Inside a container, this machine is the container. Traffic arriving through a published port originates outside that namespace, so a server bound to localhost refuses it — while working correctly for anything inside the container.
Binding to 0.0.0.0 means "accept connections on every interface", which is what a container needs. For ASP.NET Core, setting ASPNETCORE_HTTP_PORTS=8080 binds all interfaces on that port. Writing ASPNETCORE_URLS=http://localhost:8080 is the version that breaks; ASPNETCORE_URLS=http://0.0.0.0:8080 does not. The Next.js frontend has the same requirement, which is why its container image sets HOSTNAME=0.0.0.0.
When a container works and you cannot reach it, work through this in order rather than guessing:
Confirm the container is running
docker ps, not docker ps -a. A container that exited seconds after starting looks similar to a networking problem from the outside.
Read the PORTS column
It should show the mapping you asked for. Nothing there means the container was started without -p, which no amount of client-side troubleshooting will fix.
Check what the application says it bound to
docker logs on the container. The start-up lines name the address and port. If the port differs from the container side of your mapping, the mapping is pointing at nothing.
Try it from inside the container
If a request from inside succeeds and the same request from the host fails, the problem is the binding or the mapping. If it fails inside too, the application is not serving, and the network was never the issue.
Check the port number the image expects
Recent .NET container images default to 8080. Older ones defaulted to 80. Mapping to the wrong one produces exactly the same refused connection as everything else in this list.
# 1. Running, and mapped how?
docker ps
# 2. What did the application bind to?
docker logs employees-api | head -20
# 3. Does it answer from inside the container?
docker exec employees-api curl -s -o /dev/null -w "%{http_code}\n" \
http://localhost:8080/health
# 4. Does it answer from the host?
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/health
# 5. Is something else already holding the host port?
docker ps --format "{{.Names}} {{.Ports}}"- Step 3 uses localhost deliberately, because inside the container localhost is the right thing to use — it is that container.
- If step 3 returns 200 and step 4 does not, you have narrowed it to two possibilities: a binding restricted to localhost inside the container, or a mapping pointing at the wrong container port.
- If step 3 fails as well, stop looking at networking. The application is not answering, and the logs from step 2 are where the reason is.
- Step 5 lists what every container has published. A host port can only be held once, so a forgotten container from an earlier experiment shows up here.
Summary
- A container has its own port numbers, so publishing connects a host port to a container port
- -p takes the host port first and the container port second
- An application bound to localhost inside a container refuses forwarded traffic — bind 0.0.0.0
- Adding 127.0.0.1 to the host side keeps a published port off the surrounding network
- Diagnose in order: running, mapped, what it bound to, reachable from inside, then the expected port number
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Map the same container three ways
Start the same image three times: once with -p 8080:8080, once with -p 5001:8080, and once with the numbers reversed as -p 8080:5001 (using a different host port so it starts).
Request each one from the host and note which work. Then read docker ps and match the PORTS column to what you observed.
Show solution
The first two work, because the container side matches the port the application listens on. The reversed one starts happily and refuses connections, because it forwards to a port inside the container where nothing is listening.
The lesson is that Docker cannot tell you the mapping is wrong. It has no idea which port your application chose, so a mistake here produces a working container that appears broken.
docker run -d --name map-a -p 8080:8080 anvi/employees-api:1.4.0
docker run -d --name map-b -p 5001:8080 anvi/employees-api:1.4.0
docker run -d --name map-c -p 5002:5001 anvi/employees-api:1.4.0
curl -i http://localhost:8080/health
curl -i http://localhost:5001/health
curl -i http://localhost:5002/health
docker ps
docker rm -f map-a map-b map-cThink about it
Think about it
An API runs fine on a developer's machine. In a container it starts, logs no errors, and reports listening on http://localhost:8080. The container is started with -p 8080:8080 and the browser reports a refused connection.
What is happening, and why did the same configuration work outside a container?
Show solution
The application is bound to localhost inside the container, so it only accepts connections that originate inside that container. Traffic forwarded from the host arrives from outside the namespace and is refused.
Outside a container the same setting worked because the browser and the server were on the same machine, so localhost meant the same thing to both. Containerising the application split those two meanings apart without changing a line of code.
The fix is to bind all interfaces inside the container. If you want to restrict who can reach it, do that on the host side of the mapping, with -p 127.0.0.1:8080:8080 — the restriction belongs where the boundary actually is.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.