Skip to content

07 - Networks and Ports

What this session is

About 45 minutes. How containers talk to your host, to each other, and to the internet. The four network types Docker creates, why containers can find each other by name, and how to debug "why can't service A reach service B?"

The mental model

Each container gets its own network namespace - its own IP address, its own ports, its own loopback. Two consequences:

  1. The container's localhost is not your host's localhost. They're separate worlds.
  2. To reach a container's port from outside, you have to publish it with -p (host port forwards to container port).

Networks tie containers together. Containers on the same Docker network can see each other; containers on different networks can't.

The default networks

docker network ls

You'll see at least:

Network Driver What it's for
bridge bridge Default for containers without --network
host host Container shares the host's network (no isolation)
none null No networking at all

The default bridge (bridge) is what containers join automatically. Containers on the default bridge get IPs but cannot find each other by name (a quirky default).

Create a user-defined network

docker network create mynet
docker network ls

Now run containers on it:

docker run -d --name db --network mynet -e POSTGRES_PASSWORD=secret postgres:16
docker run -d --name web --network mynet -p 8080:80 nginx

Both db and web are on mynet. They can reach each other by container name:

docker exec -it web ping db          # works - Docker resolves "db" to its IP

This is the right pattern for multi-container apps. Always create a user-defined network. Don't rely on the default bridge.

Port publishing: -p HOST:CONTAINER

A container on a Docker network is reachable from other containers on the same network. To reach it from your host (or from outside your machine), you must publish the port:

docker run -d --name web --network mynet -p 8080:80 nginx

Now http://localhost:8080 on your host hits the nginx in the container.

Options:

docker run -p 8080:80 nginx                   # any interface on host port 8080
docker run -p 127.0.0.1:8080:80 nginx         # only loopback on host
docker run -p 8443:443 -p 8080:80 nginx       # publish multiple ports
docker run -P nginx                            # publish all EXPOSE'd ports to random host ports

docker port web shows the actual host port mapping.

Two containers talking

A canonical pattern: app and database.

docker network create mynet

docker run -d --name db --network mynet \
  -e POSTGRES_PASSWORD=secret \
  -v pgdata:/var/lib/postgresql/data \
  postgres:16

docker run -d --name app --network mynet -p 8080:8080 \
  -e DATABASE_URL=postgres://postgres:secret@db:5432/postgres \
  my-app:1.0

Inside the app container, db resolves to the database container's IP. Port 5432 is reachable from app (containers on the same network can reach any port without explicit publishing). Outside the cluster, only port 8080 (on the host) is exposed.

host networking (Linux only)

docker run --network host -d nginx

nginx is on the host's network - port 80 binds directly to host port 80. No port mapping needed. No isolation; the container can see and use any host network interface.

Useful for performance-sensitive networking (lower overhead than the bridge). Not available on macOS/Windows because Docker runs in a VM there.

Inspect a network

docker network inspect mynet

Shows all containers attached, their IPs, the subnet, etc.

docker network inspect mynet --format='{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

Useful for "what IP did Docker give my container?"

Disconnect, reconnect

docker network connect mynet existing_container
docker network disconnect mynet existing_container

Useful for testing failure scenarios (disconnect the database, see how the app behaves).

DNS inside containers

Docker runs an embedded DNS resolver. Inside a container on a user-defined network:

docker exec -it app cat /etc/resolv.conf
# nameserver 127.0.0.11

The 127.0.0.11 is Docker's embedded DNS. It resolves container names + external hostnames.

Test:

docker exec -it app nslookup db
# Should return the db container's IP
docker exec -it app nslookup google.com
# Should return Google's IP

If DNS resolution fails, networking is broken in some way. Common cause: container is on the default bridge (no name resolution). Recreate on a user-defined network.

Debugging "service A can't reach service B"

When connectivity isn't working:

  1. Are they on the same network?

    docker inspect <name> --format='{{json .NetworkSettings.Networks}}' | jq
    
    Both should list the same network.

  2. Can A's container resolve B's name?

    docker exec -it A nslookup B
    
    If "no such host," they're not on the same network OR A is on the default bridge.

  3. Can A reach B's port?

    docker exec -it A nc -zv B 5432       # netcat: connect test, verbose
    
    Success: port is open. "Connection refused": B isn't listening yet (race) or B's app crashed.

  4. Is B's app actually listening?

    docker exec -it B netstat -tln       # or ss -tln
    
    Some images don't include netstat or ss; install or skip.

  5. Read B's logs:

    docker logs B
    
    Did it start cleanly? Is it bound to 0.0.0.0 (all interfaces) and not 127.0.0.1 (only its own loopback)?

The single most-common bug: an app inside a container binding to 127.0.0.1 instead of 0.0.0.0. Only the container's own loopback can reach it. The fix: configure the app to bind to all interfaces.

Exercise

  1. Create a network and ping by name:

    docker network create test-net
    docker run -d --name a --network test-net alpine sleep 60
    docker run -d --name b --network test-net alpine sleep 60
    docker exec -it a ping -c 3 b
    docker stop a b && docker rm a b
    docker network rm test-net
    

  2. Two-container app:

    docker network create app-net
    docker run -d --name pg --network app-net \
      -e POSTGRES_PASSWORD=secret postgres:16
    docker run -it --rm --network app-net postgres:16 \
      psql -h pg -U postgres -c "SELECT 1"
    docker stop pg && docker rm pg
    docker network rm app-net
    

  3. Port publishing experiment:

    docker run -d --rm --name web -p 8080:80 nginx
    curl -s http://localhost:8080 | head -n 5
    docker stop web
    

  4. Default bridge gotcha:

    docker run -d --name x alpine sleep 60       # default bridge
    docker run -it --rm alpine ping -c 1 x       # FAILS - default bridge has no name resolution
    docker stop x && docker rm x
    

What you might wonder

"Can I make a container available outside my machine?" Yes - -p 80:80 binds to all interfaces by default, including external ones. Anyone on your network can reach http://your-machine-ip:80. Firewall accordingly. To bind only to localhost: -p 127.0.0.1:80:80.

"What about IPv6?" Possible but disabled by default. Enable in Docker daemon config. Beyond beginner scope.

"Why is the default bridge so awkward?" Historical reasons. The default bridge was the original; user-defined networks were added later with better defaults. The original is kept for backwards compatibility but isn't recommended for new use.

"What about overlay networks?" For multi-host setups (containers on different machines, like in Docker Swarm or Kubernetes). Beyond beginner scope; recognize the name.

Done

  • Understand container network isolation.
  • Create user-defined networks for multi-container apps.
  • Use container names for service discovery.
  • Publish ports with -p.
  • Debug network issues (DNS, port binding, connection refused).

Next: Docker Compose →

Comments