Chapter 5 — Networking Basics
Every container you've run so far has been somewhat isolated from the world.
You used -p to poke a hole in that isolation for your browser,
but there's a lot more to Docker networking. How do containers talk to each
other? Why does localhost behave differently inside a container?
How do you connect a web app container to a database container?
This chapter answers those questions and gives you the networking knowledge
you need for the practical scenarios in Chapters 7–9.
1. Port Mapping in Depth
A container has its own network namespace — it's like a tiny machine with
its own network interface and its own set of ports. A service listening on
port 80 inside a container is not the same as port 80 on your host. To make
the container's port reachable from outside, you map it with -p.
Your host machine
localhost
:8080
→ mapped to container :80
:3306
→ mapped to container :3306
:5432
not mapped — not reachable
-p HOST:CONTAINER
Inside the container
nginx / mysql
:80
nginx listening here
:3306
mysql listening here
:5432
postgres listening here
# Basic mapping: host 8080 → container 80
docker run -d -p 8080:80 nginx:alpine
# Same port on both sides (common for dev servers)
docker run -d -p 3000:3000 node:20-alpine
# Multiple ports at once
docker run -d -p 80:80 -p 443:443 nginx:alpine
# Bind to a specific host IP — only localhost can reach it, not the network
docker run -d -p 127.0.0.1:8080:80 nginx:alpine
# Let Docker choose a random available host port
docker run -d -p 80 nginx:alpine
docker port my-container # find out which port was chosen
# See port mappings for a running container
docker port my-nginx
Exposing to the whole network vs localhost only.
By default -p 8080:80 binds to 0.0.0.0 — every
network interface on your host, meaning anyone on your local network can
reach the container. Use -p 127.0.0.1:8080:80 if you only
want it accessible from your own machine. This matters especially for
databases — never expose MySQL's 3306 to the network unless you specifically
need remote access.
2. Docker's Built-in Networks
Every Docker installation comes with three networks pre-created. You can
see them with docker network ls:
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
b2c3d4e5f6a7 host host local
c3d4e5f6a7b8 none null local
bridge (default)
All containers join this unless told otherwise
Containers can reach each other by IP address
Cannot reach each other by name (DNS doesn't work on the default bridge)
Containers are isolated from the host network
Use port mapping (-p) to expose ports
host
Container shares the host's network interface directly
No port mapping needed — container ports ARE host ports
Less isolation — container can see all host network traffic
Linux only (no effect on Docker Desktop for Windows/Mac)
Useful for high-performance or monitoring containers
none
No networking at all
Container has only a loopback interface
Cannot reach the internet or other containers
Used for batch jobs that process files and don't need network access
The important limitation of the default bridge: Two containers
on the default bridge network can only talk to each other using IP addresses
(e.g. 172.17.0.3), not names. IP addresses change every time a
container restarts, making them unreliable. The solution is a custom network —
covered in the next section.
3. Custom Networks — Containers Finding Each Other by Name
When you create a custom network, Docker enables its built-in DNS for that
network. Any container on the network can reach any other container simply by
using its name as a hostname. No IP addresses, no guessing,
no fragile configuration.
Custom network: app-network
webapp
172.20.0.2
Can reach "db" by name
db
172.20.0.3
Can reach "webapp" by name
cache
172.20.0.4
Can reach "db" and "webapp"
↕ Docker DNS — containers resolve each other by --name
other-container
On default bridge — cannot reach app-network containers by name
Terminal — web app + database on a custom network
# Step 1: create the network
$ docker network create app-network
d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8
# Step 2: start the database on the network
$ docker run -d \
--name db \
--network app-network \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=myapp \
-v db-data:/var/lib/mysql \
mysql:8.0
# Step 3: start the web app on the same network
$ docker run -d \
--name webapp \
--network app-network \
-p 8080:80 \
-e DB_HOST=db \
-e DB_NAME=myapp \
-e DB_PASSWORD=secret \
myapp:latest
# The webapp can now connect to MySQL using hostname "db"
# Connection string inside webapp: mysql://root:secret@db:3306/myapp
# Prove it — ping db from webapp by name
$ docker exec webapp ping -c 2 db
PING db (172.20.0.3): 56 data bytes
64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.142 ms
64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.098 ms
Always use custom networks for multi-container setups.
The default bridge network's lack of DNS is a gotcha that catches many beginners.
Get into the habit of creating a network first and attaching all related
containers to it. Docker Compose (Chapter 10) does this automatically.
4. The localhost Confusion
This trips up almost everyone coming to Docker for the first time.
localhost means different things depending on where you use it:
✓ Your browser on the host
You visit http://localhost:8080
→ Reaches container via port mapping ✓
✗ Inside a container
App inside container connects to localhost:3306
→ Hits the container's own loopback, not your host ✗
✗ Container to container (default bridge)
webapp tries to reach db at localhost:3306
→ Hits webapp's own loopback — db is not there ✗
✓ Container to container (custom network)
webapp connects to db:3306
→ Docker DNS resolves "db" to the right container ✓
Reaching the host from inside a container
Sometimes you need a container to reach a service running directly on your
host machine (not in another container). localhost won't work —
use these special hostnames instead:
# On Windows and Mac (Docker Desktop):
host.docker.internal # resolves to the host machine's IP
# On Linux — add this flag when running the container:
docker run --add-host=host.docker.internal:host-gateway myapp
# Example: container connecting to a database running directly on the host
docker run -e DB_HOST=host.docker.internal myapp
5. Connecting Containers to Networks
A container can be connected to multiple networks simultaneously, and you
can connect or disconnect existing containers without restarting them:
# Connect a running container to an additional network
docker network connect app-network my-container
# Disconnect from a network
docker network disconnect app-network my-container
# A container can be on multiple networks at once
# (useful for a proxy that bridges a public and private network)
docker network connect public-net proxy-container
docker network connect private-net proxy-container
6. Inspecting Networks
# List all networks
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
d3e4f5a6b7c8 app-network bridge local
b2c3d4e5f6a7 host host local
# Inspect a network — see which containers are attached
$ docker network inspect app-network
[{
"Name": "app-network",
"Driver": "bridge",
"Subnet": "172.20.0.0/16",
"Containers": {
"a91b...": { "Name": "db", "IPv4Address": "172.20.0.3/16" },
"b82c...": { "Name": "webapp", "IPv4Address": "172.20.0.2/16" }
}
}]
# Remove a network (all containers must be disconnected first)
$ docker network rm app-network
# Remove all unused networks
$ docker network prune
7. Network Commands Reference
| Command | What it does |
| docker network ls | List all networks |
| docker network create NAME | Create a new network (bridge driver by default) |
| docker network inspect NAME | Show full details including connected containers and their IPs |
| docker network connect NET CONTAINER | Attach a running container to a network |
| docker network disconnect NET CONTAINER | Detach a container from a network |
| docker network rm NAME | Delete a network (must be empty first) |
| docker network prune | Remove all networks not used by any container |
| docker port CONTAINER | Show port mappings for a container |
8. Putting It Together — WordPress + MySQL
WordPress connecting to MySQL is a classic two-container setup that uses
everything from this chapter: a custom network, named volumes, port mapping,
and environment variables passed as connection details. This is also a preview
of what Docker Compose (Chapter 10) automates.
# 1. Create a dedicated network
docker network create wordpress-net
# 2. Start MySQL on the network — note: NOT exposing port 3306 to host
# (only WordPress needs to reach it, not the outside world)
docker run -d \
--name mysql \
--network wordpress-net \
-v mysql-data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=rootsecret \
-e MYSQL_DATABASE=wordpress \
-e MYSQL_USER=wpuser \
-e MYSQL_PASSWORD=wpsecret \
--restart unless-stopped \
mysql:8.0
# 3. Start WordPress on the same network
# DB_HOST = "mysql" — the container name, resolved by Docker DNS
docker run -d \
--name wordpress \
--network wordpress-net \
-p 8080:80 \
-v wp-data:/var/www/html \
-e WORDPRESS_DB_HOST=mysql:3306 \
-e WORDPRESS_DB_USER=wpuser \
-e WORDPRESS_DB_PASSWORD=wpsecret \
-e WORDPRESS_DB_NAME=wordpress \
--restart unless-stopped \
wordpress:latest
# Visit http://localhost:8080 — WordPress setup wizard
# MySQL is not exposed publicly — only reachable by WordPress inside the network
Security note: MySQL's port 3306 is not mapped to the host
in this example — it's only reachable from inside wordpress-net.
This is good practice: only expose ports that external users or your browser
actually need to reach. Inter-container communication happens on the private network.
Exercises
- Explore the default bridge. Start two containers:
docker run -d --name c1 nginx:alpine and docker run -d --name c2 nginx:alpine. Run docker network inspect bridge and note the IP addresses assigned to each. Try docker exec c1 ping -c 2 c2 — it will fail by name but work if you use the IP address directly. This demonstrates the default bridge's DNS limitation.
- Create a custom network and prove DNS works. Run
docker network create test-net, then start c1 and c2 with --network test-net. Now repeat docker exec c1 ping -c 2 c2 — this time it should work using the name. Run docker network inspect test-net to see both containers listed.
- Understand the port binding options. Start nginx with
-p 127.0.0.1:8080:80. Confirm you can reach it at http://localhost:8080. Then stop it and restart with -p 0.0.0.0:8080:80. Use docker port my-nginx to see the difference in the binding shown. On a machine with multiple network interfaces, the first is only accessible locally while the second is accessible from the network.
- Build the WordPress stack. Follow the example in Section 8 to run MySQL and WordPress together. Once WordPress is running, complete the setup wizard in your browser to create a site. Then use
docker rm -f wordpress, recreate the WordPress container with the same command, and verify that your site and setup are still intact — demonstrating that the data is in the volume, not the container.
- Use docker network inspect for debugging. With the WordPress stack running, run
docker network inspect wordpress-net. Identify the IP addresses of both containers. Then from inside the WordPress container (docker exec -it wordpress bash), try ping mysql and curl -s http://mysql:80 (which will fail — MySQL doesn't speak HTTP — but proves the name resolves). This is the approach you'd use to debug connectivity problems between containers.
Next: Chapter 6 — Writing Your First Dockerfile
So far you've used images built by others. Chapter 6 shows you how to build
your own: writing a Dockerfile that defines exactly what goes into an image,
using instructions like FROM, RUN, COPY,
WORKDIR, EXPOSE, and CMD. You'll build
a custom image, tag it, and run it — the foundation for the scenario chapters ahead.