Docker Networks

Docker & DevOps · Lesson 14 of 26 7 min read Updated

A real app is rarely one container. Your Node.js or PHP app needs to talk to MySQL, maybe to Redis, and Nginx needs to talk to your app. These containers find and reach each other through Docker networks.

In this lesson you will see why containers on the default network cannot find each other by name, create your own network where they can, and learn the special network modes and the classic beginner trap of mixing up host ports and container ports.

How container networking works

Every container gets its own network stack: its own IP address, its own localhost and its own ports. That is why two containers can both listen on port 80 without any conflict. A network is like a small private switch that containers plug into. Containers on the same network can talk to each other; containers on different networks cannot.

Docker creates three networks when it is installed:

Terminal
docker network create shop-net
docker network ls
Output
NETWORK ID     NAME       DRIVER    SCOPE
1c9a7e3f5b20   bridge     bridge    local
6d2f8a4c0e17   host       host      local
a40b6e9c3d58   none       null      local
e7f3c1a95b62   shop-net   bridge    local
  • bridge is the default network. Every container you start without --network joins it.
  • host removes network isolation: the container uses the host's network directly.
  • none gives the container no network at all, only its own localhost.
  • shop-net is the user-defined bridge network you just created.

The default bridge vs your own network

On the default bridge network, containers get IP addresses such as 172.17.0.2, but they cannot find each other by name. IP addresses change every time a container is recreated, so hard-coding them is fragile.

On a user-defined network, Docker runs a small built-in DNS server. Every container can reach every other container on the same network by its container name. This is the single most important networking fact in Docker, and the reason you should always create your own network for related containers.

Terminal
docker run -d --name db --network shop-net \
  -e MYSQL_ROOT_PASSWORD=Root-Change-Me -e MYSQL_DATABASE=shop mysql:8.4
docker run --rm --network shop-net alpine:3.22 ping -c 2 db
Output
PING db (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.091 ms
64 bytes from 172.18.0.2: seq=1 ttl=64 time=0.105 ms

--- db ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.091/0.098/0.105 ms

The name db resolved to the MySQL container's address on shop-net. Try the same ping without --network shop-net and you get ping: bad address 'db', because the default bridge has no name resolution.

Now an app on the same network connects to MySQL with the host name db and MySQL's own port, 3306:

Terminal
docker run -d --name shop-api --network shop-net -p 3000:3000 \
  -e DB_HOST=db -e DB_PORT=3306 -e DB_NAME=shop shop-api:1.0

Notice that the database container has no -p option. Containers on the same network can reach all of each other's ports directly. You publish a port only when something outside Docker (your browser, a client on your laptop) needs to reach it. Keeping the database unpublished means it is not reachable from outside at all, which is safer.

Host ports vs container ports

This trips up nearly everyone once. Suppose MySQL was started with -p 3307:3306:

Who is connectingHost namePort
A MySQL client on your laptop127.0.0.13307 (the published host port)
Another container on the same networkdb3306 (the container's own port)
Code inside the db container itselflocalhost3306

And the classic bug: inside a container, localhost means that container, not your computer and not the database container. An app configured with DB_HOST=localhost looks for MySQL inside its own container and fails with "connection refused".

Connecting, disconnecting and inspecting

A container can join several networks. This is how a reverse proxy sits on a public-facing network and an internal one at the same time:

Terminal
docker network create backend --internal
docker network connect backend db
docker network disconnect shop-net db
docker network inspect --format '{{range .Containers}}{{.Name}} {{end}}' shop-net
Output
shop-api

An --internal network has no route to the internet: containers on it can talk to each other but cannot download anything or be reached from outside. It is a good home for databases. After the commands above, db is only on backend, so shop-api would need to join backend too to reach it.

Host networking and reaching the host

With --network host, the container shares the host's network stack. There is no port publishing; an app listening on port 3000 is directly on the host's port 3000. It is slightly faster and useful for some monitoring tools, but you lose isolation and port conflicts come back. On Linux it works fully; on Docker Desktop it must first be enabled in the settings.

Sometimes a container needs to reach a service running on your computer, such as a MySQL server you installed with XAMPP. Docker Desktop provides the special host name host.docker.internal for this. On Linux, add it yourself:

Terminal
docker run --rm --add-host=host.docker.internal:host-gateway alpine:3.22 \
  ping -c 1 host.docker.internal

Common mistakes

  • Using the default bridge and expecting names to work. Create a network with docker network create and attach every related container to it.
  • DB_HOST=localhost inside a container. localhost is the container itself. Use the database container's name.
  • Connecting to the published port from another container. Containers talk to each other on the container port (3306), not the host port (3307).
  • Publishing database ports "just in case". If only other containers use the database, do not publish it at all.

Key points

  • Each container has its own IP address, ports and localhost; networks connect containers like a private switch.
  • The default bridge network has no DNS by name; user-defined networks let containers reach each other by container name.
  • Publish ports only for traffic from outside Docker; containers on the same network reach each other's ports directly.
  • --internal networks have no outside access, which suits databases; a container can join several networks.
  • host.docker.internal reaches your computer from a container (add it on Linux with --add-host=host.docker.internal:host-gateway).

Exercise

Create a network called library-net. Run an Nginx container named catalogue on it without publishing any port. Then prove that a second, throwaway Alpine container on the same network can fetch the Nginx welcome page by name using wget -qO- http://catalogue. Finally, explain why your browser cannot open the page.

Show solution
Terminal
docker network create library-net
docker run -d --name catalogue --network library-net nginx:alpine
docker run --rm --network library-net alpine:3.22 wget -qO- http://catalogue
Output
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

The Alpine container finds catalogue through the network's DNS and connects to port 80 directly. Your browser runs outside Docker, and no port was published with -p, so nothing on your computer forwards traffic to the container.

Quick Quiz

1. Why can two containers on a user-defined network reach each other by name?

2. An app container uses DB_HOST=localhost to reach a MySQL container and gets "connection refused". Why?

3. MySQL runs with -p 3307:3306. Which address should another container on the same network use?

4. What does docker network create backend --internal give you?

Learn Docker & DevOps with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call