Docker Networking: A Guide for Developers
· docker
Most developers learn Docker networking the hard way. They slap docker-compose up on a YAML file somebody else wrote, everything works, and they move on. Then one container can’t talk to another, and they spend three hours guessing at port numbers like it’s a combination lock. The root cause is almost always the same: nobody told them how the network actually works underneath the abstraction.
Let’s fix that.
What Is a Docker Network?
A Docker network is a virtual layer that wires containers together. Each container gets its own IP, its own hostname, and a direct line to every other container on the same network. Docker handles all of this quietly, which is genuinely useful — until something breaks and you’re staring at Connection refused with no idea which port you’re supposed to hit.
Three properties matter:
- Container Communication: Containers on the same network talk to each other by IP, by name, or by alias. Docker’s built-in DNS resolves the names. No
/etc/hostshacking required. - Port Exposure: Nothing is exposed to the outside world by default. You have to explicitly punch a hole. This is a good default. Treat it as a feature, not an obstacle.
- DNS Integration: Name resolution inside a Docker network just works. One container calls another by name, DNS resolves it, traffic flows. Simple is a superpower.
How Containers Actually Talk to Each Other
Here’s the thing most tutorials gloss over: containers on the same network communicate using internal ports. The port the application listens on inside the container. That’s it.
Inside the Network: Internal Ports
Port mappings like -p 8080:3000? Irrelevant inside the network. Those mappings exist solely for traffic coming from outside — your browser, your host machine, the wider internet. Containers don’t use them.
If a container called app runs a service on port 3000, any other container on the same network reaches it at app:3000. Not app:8080. Not localhost:8080. Just app:3000.
Why does this trip people up? Because -p 8080:3000 looks like it’s defining the port. It’s not. It’s defining a forwarding rule for external traffic. Internal traffic never touches it.
DNS and Service Names
Docker runs an internal DNS server for each network. Every container gets a hostname equal to its name. You don’t configure this. You don’t install anything. It’s just there.
Container Names
Name a container db, and every other container on the network can resolve db as a hostname. Connect to db:5432 and you’re talking to Postgres. Clean, tight, zero ceremony.
Aliases
Sometimes the container name isn’t what you want downstream services to use. Aliases give you flexibility:
docker run --network my-network --name db --network-alias primary-db mydatabaseNow the container answers to both db and primary-db. Same container, two DNS entries. Useful when you’re migrating names or running blue-green setups without rewiring everything.
Multi-Container Setup with Compose
Docker Compose creates DNS entries from service names automatically. Look at this:
services:
app:
image: myapp
db:
image: mydatabaseThe app container connects to the database at db:5432. No network aliases to configure, no IP addresses to hardcode. Compose just wires it up. This is the part that works so well people forget there’s a network underneath.
Port Forwarding: Punching Holes to the Outside
Let’s talk about port forwarding. Everything above happens inside the Docker network. But at some point, you need the outside world to reach a container — your browser needs to hit the web app, a monitoring tool needs to scrape metrics, whatever.
That’s what -p does.
How It Works
docker run -p 8080:3000 mywebapp3000: The port the app listens on inside the container.8080: The port on the host that forwards traffic into the container.
Hit http://localhost:8080, and Docker routes the request to port 3000 inside the container. The container has no idea the outside world thinks the port is 8080. It just sees traffic on 3000.
The Split That Matters
- Inside the network: containers use internal ports (
3000). - Outside the network: the host and internet use the mapped port (
8080).
Confuse these two, and you’ll spend an afternoon debugging something that isn’t broken. Ask me how I know.
Port Forwarding in Docker Compose
services:
web:
build: .
ports:
- "8080:3000"This maps port 3000 inside web to port 8080 on the host. But — and this is the part people miss — other containers on the same network still use web:3000. The port mapping is purely an external concern. Internal traffic doesn’t care about it. Won’t use it. Can’t use it.
Internal vs. External Access
Two worlds. Keep them separate in your head.
Internal Access
Containers on the same network use the container name (or alias) plus the internal port. A web server in webapp listening on 3000? Other containers reach it at webapp:3000. No port mapping involved. No host networking. Just a name and a port.
External Access
To let the host or the internet reach a container:
- Map the container’s port to a host port (
-p 8080:3000). - Access the service through the host port (
http://localhost:8080).
That’s the entire model. Internal: name plus internal port. External: host plus mapped port. If you remember nothing else, remember this split.
Putting It All Together
Enough theory. Let’s wire up a web app that talks to a database.
The Setup
- Web app listens on port
5000. - Database listens on port
5432.
The Steps
-
Create the network:
docker network create my-app-network -
Start the database:
docker run --network my-app-network --name db --network-alias primary-db mydatabaseReachable as
db:5432by name. Also reachable asprimary-db:5432by alias. -
Start the web app:
docker run --network my-app-network --name webapp mywebapp -
Internal communication: The web app connects to the database at
db:5432orprimary-db:5432. No port mapping. No host involvement. Just two containers on the same network, talking directly. -
Expose the web app externally:
docker run -p 8080:5000 --network my-app-network --name webapp mywebappNow the outside world reaches the web app at
http://localhost:8080. The database stays hidden — no port mapping, no exposure, no attack surface. Exactly how it should be.
The Verdict
Docker networking is genuinely simpler than most people think. The confusion comes from one place: conflating internal ports with external port mappings. Once you separate those two concepts, everything clicks.
Internal ports are for containers talking to containers. Port mappings are for the outside world talking to containers. DNS handles name resolution so you never hardcode an IP. Compose automates the wiring so you rarely think about networks at all.
That’s the whole model. It’s lean, it’s earned, and it works. Go build something with it.
