Networking Basics

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:

Terminal
$ 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

Terminal
# 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

CommandWhat it does
docker network lsList all networks
docker network create NAMECreate a new network (bridge driver by default)
docker network inspect NAMEShow full details including connected containers and their IPs
docker network connect NET CONTAINERAttach a running container to a network
docker network disconnect NET CONTAINERDetach a container from a network
docker network rm NAMEDelete a network (must be empty first)
docker network pruneRemove all networks not used by any container
docker port CONTAINERShow 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
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.