Skip to content

Phase 2 · CONTAINERS & CI/CD

Docker networking & port mapping

Day 25 of 90 ~50 min 0/25 in phase Builds on Day 24

By the end of today

  • Publish a container port to your host with -p and verify it
  • Connect containers by name on a user-defined bridge network
  • Explain EXPOSE versus -p and Docker's default bridge in interviews

Docker networking: published ports and names

Section 1 of 5 · ~3 min

A container starts life sealed off. Day 22 called that isolation a feature — each container gets its own network namespace and a private IP on a virtual network Docker builds for it. That isolation immediately raises two practical questions, and today answers both: how does the outside world reach a service inside a container, and how do two containers reach each other?

Publishing a port lets the outside in. When you install Docker it creates a virtual switch called the default bridge (you’ll see it as docker0), and every container you run lands on it with a private address like 172.17.0.2. That address is real, but it’s only reachable from Docker’s internal network — your laptop and the internet can’t see it. To expose a service you publish its port with -p host:container. docker run -p 8080:80 nginx wires host port 8080 to the container’s port 80: Docker adds the NAT rules so a request to localhost:8080 is forwarded inside. Read the mapping host-first, container-second — flipping the two numbers is the classic beginner bug. And note what doesn’t publish a port: an EXPOSE 80 line in a Dockerfile is pure documentation. It records which port the app listens on; only -p at run time actually opens the door.

Container-to-container: reach each other by name

Publishing is for traffic from outside. Containers that need to talk to each other don’t need a published port at all — they share an internal network. The catch is the default bridge: containers on it can only reach each other by raw IP address, which changes on every restart. The fix is a user-defined network: docker network create appnet. On a user-defined bridge Docker runs an embedded DNS server (at 127.0.0.11), so a container reaches a peer by its namecurl http://api just works, with no IP addresses hard-coded. docker network ls lists every network; docker network create makes your own. Put an app and its database on one network and they find each other by name, while staying isolated from everything else on the host.

Docker networking: your laptop reaches the host on port 8080, which -p 8080:80 forwards into the web container's port 80; web and api sit on the user-defined bridge appnet and reach each other by name via Docker's embedded DNS. appnet — user-defined bridge (DNS) your laptop curl :8080 host :8080 web :80 api :80 localhost -p 8080:80 by name
Outsiders come through the published port; neighbours reach each other by name on the network.

Real world: A published port is the street-door number on the mailbox out front — one public address the postal service can deliver to, which the lobby forwards to a specific apartment. Container-to-container names are the internal intercom: residents buzz each other by apartment name without anyone stepping out to the street. Outsiders use the street door; neighbours use the intercom. Wire them up wrong and the pizza either never arrives or gets buzzed to the wrong flat.

The official WordPress image shows both halves at once. In its standard docker compose setup the WordPress container reaches its MySQL container at the hostname db — that’s container-to-container by name on the network Compose creates for you — while only WordPress publishes -p 8080:80 so you can open the site in a browser. The database is deliberately never published: peers reach it by name, and the outside world can’t reach it at all.

Hands-On Lab

Section 2 of 5 · ~3 min

Budget about 25 minutes. Open your WSL2 Ubuntu 24.04 terminal with Docker 27+ installed (Days 22–24 set this up; macOS users can use Docker Desktop’s terminal). Type each command yourself and read every line of output — today you publish a port, then wire two containers together by name.

# 1. List the networks Docker created for you. Every install ships these three.
docker network ls
# Output (the NETWORK ID column is random — yours will differ):
# NETWORK ID     NAME      DRIVER    SCOPE
# 3f2a1b0c9d8e   bridge    bridge    local
# a1b2c3d4e5f6   host      host      local
# 9f8e7d6c5b4a   none      null      local
# 2. Run nginx in the background and publish container port 80 to host port 8080 (host:container).
docker run -d --name web -p 8080:80 nginx:alpine
# Output (a fresh image pull, then the new container's 64-char ID — the pull lines and ID will differ):
# Unable to find image 'nginx:alpine' locally
# alpine: Pulling from library/nginx
# ...
# Status: Downloaded newer image for nginx:alpine
# 7b9c2e5a4d3f1a08c6b5e4d3c2b1a0f9e8d7c6b5a4938271605f4e3d2c1b0a9f
# 3. Prove the mapping works: hit the host port and nginx answers from inside the container.
curl -sI localhost:8080 | head -n 1
# Output:
# HTTP/1.1 200 OK
# 4. Read the port mapping straight from Docker — container port -> host binding.
docker port web
# Output:
# 80/tcp -> 0.0.0.0:8080
# 80/tcp -> [::]:8080
# 5. docker ps shows the same mapping in the PORTS column.
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
# Output:
# NAMES     IMAGE          PORTS
# web       nginx:alpine   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
# 6. Create your OWN bridge network. Unlike the default bridge, it gives containers name-based DNS.
docker network create appnet
# Output (the new network's ID — yours will differ):
# 5c4b3a2190f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a3928170605a
# 7. Confirm it exists alongside the built-ins.
docker network ls
# Output:
# NETWORK ID     NAME      DRIVER    SCOPE
# 3f2a1b0c9d8e   bridge    bridge    local
# 5c4b3a2190f8   appnet    bridge    local
# a1b2c3d4e5f6   host      host      local
# 9f8e7d6c5b4a   none      null      local
# 8. Put a second nginx on appnet, named "api". No -p — it doesn't need a host port to talk to peers.
docker run -d --name api --network appnet nginx:alpine
# Output (container ID — yours will differ):
# 2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0
# 9. Run a small client on the SAME network so it can see api. sleep keeps it alive to exec into.
docker run -d --name client --network appnet busybox sleep 1d
# Output (busybox pulls on first use, then the container ID — all yours will differ):
# Unable to find image 'busybox:latest' locally
# latest: Pulling from library/busybox
# 9ad63333ebc9: Pull complete
# Digest: sha256:… (yours will differ)
# Status: Downloaded newer image for busybox:latest
# f0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
# 10. From the client, reach "api" by its container name — Docker's embedded DNS resolves it to api's IP.
docker exec client ping -c 2 api
# Output (the 172.x address Docker assigned api is arbitrary — yours will differ):
# PING api (172.19.0.2): 56 data bytes
# 64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.089 ms
# 64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.104 ms
# --- api ping statistics ---
# 2 packets transmitted, 2 packets received, 0% packet loss
# round-trip min/avg/max = 0.089/0.096/0.104 ms   (times yours will differ)
# 11. Inspect appnet — every container attached, with the IP Docker handed it from the subnet.
docker network inspect appnet -f '{{range .Containers}}{{.Name}} = {{.IPv4Address}}{{"\n"}}{{end}}'
# Output (the 172.x addresses are what Docker picked — yours will differ):
# api = 172.19.0.2/16
# client = 172.19.0.3/16
# 12. Clean up: force-remove the three containers, then remove the network you created.
docker rm -f web api client && docker network rm appnet
# Output (Docker echoes each name it removed):
# web
# api
# client
# appnet

Read the last outputs back: -p 8080:80 let your laptop reach web from outside through the host port, while api — with no published port at all — was reachable from client by name because both sat on the same user-defined network. Outside traffic uses -p; container-to-container uses names on a shared network. That split is the whole of Docker networking.

Common Errors & Fixes

Section 3 of 5 · ~3 min

These three trip up almost everyone in their first week wiring containers together. Read the error text slowly — learning to parse it is the actual skill.

Common error: Publishing a host port that is already in use — a second docker run -p 8080:80 … while web still holds 8080 — prints:

docker: Error response from daemon: driver failed programming external connectivity on endpoint api: Bind for 0.0.0.0:8080 failed: port is already allocated.

Why: Only one process can bind a given host port at a time. Host port 8080 is already taken — by the first container, or by something running directly on the host — so Docker can’t program the second mapping and refuses to start the container.

Fix: Pick a different host port (-p 8081:80) or free the one in use. Find the culprit with docker ps (look at the PORTS column) or ss -tlnp | grep ':8080' for a non-Docker process, then stop it before reusing the port.

How you’d spot it in prod: A container stuck in a restart loop with port is already allocated in docker logs or the orchestrator’s events usually means two services claim the same host port, or a previous container wasn’t cleaned up. Check what already owns the port before changing the app.

Common error: Expecting container names to resolve on the default bridge — reaching api from a container that was started without --network appnet — prints:

curl: (6) Could not resolve host: api

Why: Docker’s automatic DNS only runs on user-defined networks. On the default bridge there is no name resolution, so api is just an unknown hostname. The container it names is running fine — it simply can’t be found by name from the default bridge.

Fix: Put both containers on the same user-defined network: docker network create appnet and start each with --network appnet. Then curl http://api resolves. In real projects docker compose creates that network for you and every service is reachable by its service name.

How you’d spot it in prod: Could not resolve host (or Name or service not known) between two of your own services almost always means they aren’t on the same user-defined network — or one names the other by the wrong service name. Check docker network inspect to see who is actually attached before touching DNS.

Common error: Telling one container to reach another at localhostcurl http://localhost:80 from inside web to hit api — prints:

curl: (7) Failed to connect to localhost port 80 after 0 ms: Connection refused

Why: Inside a container, localhost means that container itself, not the host and not a peer. Each container has its own network namespace, so web’s localhost has nothing listening on 80. And -p publishes to the host, not to sibling containers — peers never reach each other through a published host port.

Fix: Address the peer by its container name on a shared user-defined network: from web, use curl http://api (not localhost). If a container genuinely needs the host machine, Docker Desktop exposes it as host.docker.internal.

How you’d spot it in prod: A service logging Connection refused to localhost or 127.0.0.1 for a dependency is the classic containerisation slip — code written for one box where everything shared a loopback. Point it at the service name (or an env-var host) instead of localhost.

Docker Networking Interview Questions

Section 4 of 5 · ~1 min

These port-and-network questions are among the most common Phase-2 Docker screeners, and a calm answer that keeps host-first, container-second straight lands better than a clever one. The answer bank renders right after this note — cover each answer, say your own version out loud first, then compare, because recalling before revealing is what makes it stick for interview day.

Go Deeper

Section 5 of 5 · ~1 min

Optional extras if you have ~25 more minutes today:

  • 5 min — Run docker network inspect bridge and read the Subnet and Gateway fields — that’s the private 172.17.0.0/16 range every default-bridge container gets an address from.
  • 10 min — Skim the Docker networking overview and the bridge network tutorial — the docs walk the exact default-bridge-versus-user-defined split you just ran.
  • 10 min — Rebuild today’s setup, then start a third container on appnet with --network-alias cache and ping both its name and its alias — see how one container can answer to more than one name, which is how blue-green swaps work.
What does the -p flag do, and how do you read host:container? Both

-p publishes a container's port to the host so traffic from outside can reach it. You read it left-to-right as host:container: -p 8080:80 means 'send anything arriving on the host's port 8080 into the container's port 80.' Without it, the container's port is only reachable from other containers on the same network, never from your laptop or the internet. The mistake I watch for is flipping the two numbers — -p 80:8080 forwards host port 80 to a container port nothing is listening on, and you get connection refused. So I always say it out loud when I type it: host first, container second.

What is the difference between EXPOSE and -p (--publish)? Both

EXPOSE is documentation. It's a line in the Dockerfile that records which port the app listens on — it opens nothing by itself and doesn't make the port reachable. -p, at run time, actually publishes the port to the host and sets up the NAT rules that forward traffic in. So EXPOSE 80 just tells the next person 'this app uses 80'; docker run -p 8080:80 is what lets you hit it. The one place EXPOSE has teeth is docker run with a capital -P, which publishes every EXPOSEd port to a random high host port. In interviews I sum it up as: EXPOSE declares, -p connects.

How do two containers talk to each other? Both

You put them on the same user-defined network and let them reach each other by container name. When I run docker network create appnet and start both containers with --network appnet, Docker runs an embedded DNS server that resolves each container's name to its current IP, so the app connects to 'db' or 'api' as a hostname — no IP addresses hard-coded. The gotcha I always flag: the default bridge network does not give name resolution, only user-defined ones do, which is one reason everyone uses compose. And you don't need -p for this — publishing is only for traffic from outside the host; container-to-container traffic stays on the internal network.

What is Docker's default bridge network, and why create your own? Product

Every Docker install ships a default network called bridge, and any container you run without --network lands on it. It works, but it has two real limits: containers on it can only reach each other by IP address, not by name, and every container shares one flat network with no isolation. A user-defined bridge, created with docker network create, fixes both — it gives you automatic DNS so containers find each other by name, and it isolates that group of containers from everything else. So for anything beyond a one-off container I create a network per application, which is exactly what docker compose does for you automatically.

Mark Day 25 complete

Your containers can talk now — tomorrow you make their data survive a restart with volumes and persistent storage.

Stuck on today’s lab? Ask in Mission 90 Q&A