# Docker: container networking and the embedded DNS

## Concept

Two containers on the same host can be one virtual cable apart or utterly
unable to exchange a packet, and the only thing that decides which is
*network membership* — a piece of Docker bookkeeping, not anything
physical. This exercise makes that boundary visible from both sides: the
name-resolution side (Docker runs a private DNS server at `127.0.0.11`
inside each container, and it answers only for containers you share a
network with) and the packet side (iptables rules that silently drop
traffic between bridges). You'll watch the *same container name* resolve to
two different addresses depending on who is asking, attach a running
container to a second network and watch its name start resolving with no
restart, and confirm that publishing a port does exactly nothing for
container-to-container traffic.

## Mental model

### Two questions, not one

`ping other-container` is the command everyone reaches for, and it's a bad
diagnostic, because it can fail for two unrelated reasons and reports both
the same way. Split it in half and keep the halves separate for the whole
exercise:

| Question | Command | What it exercises |
| --- | --- | --- |
| **Does the name resolve?** | `getent hosts <name>` (or `nslookup <name>`) | Only DNS. Prints an address and exits `0`, or prints nothing and exits `2`. No packets are sent to the target at all. |
| **Can packets get there?** | `ping <ip>`, `wget -O- http://<ip>/` | Only reachability. Skips DNS entirely by using the raw address. |

Those two answers are independent, and the interesting configurations are
the ones where they disagree. The **default** `bridge` network is exactly
such a case, and it's Part 2 below.

The *shape* of a failure is also information. There are four distinct
signatures and they mean genuinely different things:

| What you see | What it means |
| --- | --- |
| `ping: bad address 'name'` / `getent` exits `2` / `NXDOMAIN` | DNS said no. Nothing was ever sent to the target. |
| `Connection refused` (instant) | Packets arrived. The host is reachable; nothing is listening on that port. This is a *success* for networking. |
| Hangs, then `download timed out` / `100% packet loss` | Packets left, and vanished. Something dropped them silently — a firewall rule, which is what network isolation is made of. |
| `Network unreachable` (instant) | The container has no route to that address at all. Its own kernel refused to try. |

"Refused" versus "timed out" is the single most useful distinction in the
list: the first means your networking is fine and your service isn't, the
second means the packets never got there.

### The four network modes

| Mode | What it actually does | Reach for it when... |
| --- | --- | --- |
| **User-defined bridge**<br>`docker network create X` | Its own Linux bridge and subnet, plus an embedded DNS server at `127.0.0.11` in every attached container that resolves the names, hostnames and aliases of the other members. Containers can be attached and detached while running. | Essentially always for multi-container apps. This is what `docker compose` creates for you per project, which is why service names "just work" there and people never learn that the DNS is a property of the network. |
| **Default `bridge`**<br>(what you get with no `--network`) | One shared legacy bridge on `172.17.0.0/16`. Every container on the host lands here by default and can reach every other one by IP. **No embedded DNS** — `/etc/resolv.conf` points straight at the host's upstream resolver, so container names mean nothing. | Never, deliberately. It exists for backward compatibility with `--link`. Its two failure modes are that names don't resolve and that isolation is nil — every unrelated container on the box is one IP away. |
| **`--network none`** | A network namespace with nothing in it but loopback. No `eth0`, no route, no gateway. | Batch jobs that process local data and have no business talking to anything: untrusted format conversion, a build step, a fuzzing target. |
| **`--network host`** | No network namespace at all — the container shares the host's stack. `-p` becomes meaningless; a bind to `:8080` binds the host's `:8080`. | Performance-critical or port-scanning-ish workloads where the NAT hop matters. Note it doesn't work the way you expect on macOS/Windows, where "the host" is a Linux VM, not your laptop. |

### The picture

```
   net-blue  172.20.0.0/16                 net-green  172.21.0.0/16
  ┌───────────────────────────┐          ┌──────────────────────────┐
  │  net-alpha    172.20.0.2  │          │  net-gamma   172.21.0.2  │
  │  net-beta     172.20.0.3  │          │                          │
  │                           │          │                          │
  │  DNS 127.0.0.11 knows:    │    ╳     │  DNS 127.0.0.11 knows:   │
  │    net-alpha, net-beta    │  no route│    net-gamma             │
  └───────────────────────────┘  no DNS  └──────────────────────────┘

  Same host. Same kernel. Same bridge driver. The only difference
  between "one hop away" and "unreachable" is which box a name is in.
```

## Setup

Two terminals, side by side. Everything below is `alpine`-based, so the
tools (`ping`, `getent`, `nslookup`, `wget`, `ip`) are busybox applets that
are already there — nothing to install. `nginx:alpine` is used so each
container is also *serving* something on port 80, which makes reachability
testable with more than ICMP.

```bash
docker network create net-blue
docker network create net-green

docker run -d --name net-alpha --network net-blue  nginx:alpine
docker run -d --name net-beta  --network net-blue  nginx:alpine
docker run -d --name net-gamma --network net-green nginx:alpine
```

Note what is *not* in those `docker run` lines: no `-p`. Nothing is
published to your host at all, and every bit of traffic in Parts 1–4 still
works. Hold onto that for Part 5.

Terminal A — a shell inside `net-alpha`:

```bash
docker exec -it net-alpha sh
```

Terminal B — a shell inside `net-gamma` (not needed until Part 3, but open
it now):

```bash
docker exec -it net-gamma sh
```

Some steps are run from your ordinary **host shell** rather than inside a
container — those are labelled.

**On macOS**, the Docker engine is a Linux VM, so container IPs like
`172.20.0.2` are not routable from your Mac's terminal — `ping 172.20.0.2`
from macOS will fail no matter how healthy the network is. Every
observation below is therefore made *from inside a container*, which is
where it belongs anyway. Your subnets may also differ from the `172.20`/
`172.21` shown here; Docker allocates them in order of network creation.

## Steps

### Part 1 — one network, and the resolver that comes with it

What this part tests: the baseline. Two containers on one user-defined
bridge, checking the two questions separately — name resolution first, then
reachability — so that when they come apart in Part 2 you know what
"together" looked like.

1. **A** (`net-alpha`): find out where you are.

   ```sh
   ip -4 -o addr show eth0
   ```

   ```
   11: eth0    inet 172.20.0.2/16 brd 172.20.255.255 scope global eth0
   ```

2. **Predict**: `net-beta` is a name Docker made up. Something has to
   answer for it. What do you expect in `/etc/resolv.conf`?
3. **A**: `cat /etc/resolv.conf`
4. **Observe**:

   ```
   # Generated by Docker Engine.
   # This file can be edited; Docker Engine will not make further changes once it
   # has been modified.

   nameserver 127.0.0.11
   options ndots:0

   # Based on host file: '/etc/resolv.conf' (internal resolver)
   # ExtServers: [host(192.168.65.7)]
   ```

   `127.0.0.11` — a loopback address *inside this container's network
   namespace*, where Docker runs a small DNS server. This is the whole
   mechanism, sitting in plain sight. It answers for container names on the
   networks this container belongs to and forwards everything else to the
   real resolver listed as `ExtServers`. `ndots:0` means even a bare
   single-label name like `net-beta` gets tried as-is rather than being
   glued onto a search domain first.

5. **A**: ask it, twice, two ways:

   ```sh
   getent hosts net-beta
   nslookup net-beta
   ```

6. **Observe**:

   ```
   172.20.0.3        net-beta  net-beta
   ```

   ```
   Server:		127.0.0.11
   Address:	127.0.0.11:53

   Non-authoritative answer:
   Name:	net-beta
   Address: 172.20.0.3
   ```

   `nslookup` names the culprit explicitly: the answer came from
   `127.0.0.11`, not from your ISP, not from `/etc/hosts`.

7. **A**: now the second question — reachability, by name and by port:

   ```sh
   ping -c 3 net-beta
   wget -qO- http://net-beta/ | grep title
   ```

8. **Observe**:

   ```
   PING net-beta (172.20.0.3): 56 data bytes
   64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.128 ms
   64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.127 ms
   64 bytes from 172.20.0.3: seq=2 ttl=64 time=0.064 ms

   --- net-beta ping statistics ---
   3 packets transmitted, 3 packets received, 0% packet loss
   ```

   ```
   <title>Welcome to nginx!</title>
   ```

   Both questions answer yes. 0.1 ms — a virtual cable across a bridge in
   the same kernel.

9. **A**: for the contrast you'll need in Part 3, watch what a *reachable*
   host with no listener looks like:

   ```sh
   wget -O- -T 5 http://net-beta:9999/
   ```

   ```
   Connecting to net-beta:9999 (172.20.0.3:9999)
   wget: can't connect to remote host (172.20.0.3): Connection refused
   ```

   Instant, and explicit. `net-beta`'s kernel received the SYN and sent
   back a RST. Remember this shape.

10. From your **host** shell, see the membership Docker is tracking:

    ```bash
    docker network inspect net-blue --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}'
    ```

    ```
    net-beta 172.20.0.3/16
    net-alpha 172.20.0.2/16
    ```

    That list *is* the DNS zone. Nothing else is.

### Part 2 — the default bridge, where the two answers come apart

What this part tests: the claim in the mental model that name resolution
and reachability are independent. The default `bridge` network is the
cleanest proof, because it has no embedded DNS but perfectly ordinary
connectivity — everything works except names.

11. From your **host** shell, start two containers with no `--network` at
    all, which puts them on the default bridge:

    ```bash
    docker run -d --name net-legacy1 nginx:alpine
    docker run -d --name net-legacy2 nginx:alpine
    docker exec net-legacy2 ip -4 -o addr show eth0
    ```

    ```
    11: eth0    inet 172.17.0.6/16 brd 172.17.255.255 scope global eth0
    ```

    `172.17.x` — the default bridge's subnet. Note the address; you need it
    in a moment, and yours will differ depending on what else is running.

12. **Predict**: same host, same bridge driver, two containers one subnet
    apart. Does `net-legacy1` resolve `net-legacy2`? Does it *reach* it?
13. **Host**: `docker exec -it net-legacy1 sh`, then inside:

    ```sh
    cat /etc/resolv.conf
    getent hosts net-legacy2 ; echo "exit=$?"
    ping -c 2 net-legacy2
    ```

14. **Observe**:

    ```
    # Generated by Docker Engine.
    nameserver 192.168.65.7

    # Based on host file: '/etc/resolv.conf' (legacy)
    ```

    ```
    exit=2
    ```

    ```
    ping: bad address 'net-legacy2'
    ```

    No `127.0.0.11`. The comment Docker writes into the file even labels
    it: **`(legacy)`**. There is no embedded resolver in this container, so
    `net-legacy2` is asked of the upstream nameserver, which has of course
    never heard of it (`nslookup` spells it out: `** server can't find
    net-legacy2: NXDOMAIN`).

15. **Predict**: so is `net-legacy2` unreachable?
16. **Same shell**, using the raw address from step 11:

    ```sh
    ping -c 3 172.17.0.6
    wget -qO- http://172.17.0.6/ | grep title
    ```

17. **Observe**:

    ```
    PING 172.17.0.6 (172.17.0.6): 56 data bytes
    64 bytes from 172.17.0.6: seq=0 ttl=64 time=0.146 ms
    64 bytes from 172.17.0.6: seq=1 ttl=64 time=0.120 ms
    64 bytes from 172.17.0.6: seq=2 ttl=64 time=0.115 ms

    --- 172.17.0.6 ping statistics ---
    3 packets transmitted, 3 packets received, 0% packet loss
    ```

    ```
    <title>Welcome to nginx!</title>
    ```

    Perfectly reachable. Identical latency to Part 1. The two questions
    have come apart:

    | | Default `bridge` | User-defined `net-blue` |
    | --- | --- | --- |
    | `/etc/resolv.conf` | `nameserver 192.168.65.7` — the host's upstream | `nameserver 127.0.0.11` — Docker's embedded DNS |
    | Name resolves? | **No** — `bad address` / NXDOMAIN | **Yes** — `172.20.0.3` |
    | IP reachable? | **Yes** — 0% packet loss | **Yes** — 0% packet loss |

    This is why `ping name` is the wrong diagnostic. The identical
    `ping: bad address` you'd get from a genuinely isolated container means
    something completely different here: this network is not isolated at
    all. Every container on the default bridge — including anything else
    running on your machine right now — can reach every other one. What it
    lacks is a name service, and that's a *convenience* feature, not the
    security boundary.

### Part 3 — a different network, where nothing works and the failure looks different

What this part tests: the actual isolation boundary. `net-gamma` is on
`net-green` and shares no network with anything in `net-blue`. Both
questions should now answer no — and per the mental model, the *reason*
each one fails should be legible in the shape of the failure.

18. **B** (`net-gamma`): confirm it has an embedded resolver of its own:

    ```sh
    cat /etc/resolv.conf | grep nameserver
    ip -4 -o addr show eth0
    ip route
    ```

    ```
    nameserver 127.0.0.11
    11: eth0    inet 172.21.0.2/16 brd 172.21.255.255 scope global eth0
    default via 172.21.0.1 dev eth0
    172.21.0.0/16 dev eth0 scope link  src 172.21.0.2
    ```

    Note this carefully: `net-gamma` *does* have Docker's DNS at
    `127.0.0.11`. The resolver isn't missing here. It also has a default
    route, so it isn't route-less either.

19. **Predict**: `net-gamma` has the embedded resolver. Does
    `getent hosts net-alpha` work?
20. **B**: `getent hosts net-alpha ; echo "exit=$?"` and `nslookup net-alpha`
21. **Observe**:

    ```
    exit=2
    ```

    ```
    Server:		127.0.0.11
    Address:	127.0.0.11:53

    ** server can't find net-alpha: NXDOMAIN
    ```

    Read those two lines together — they're the sharpest thing in this
    exercise. The embedded DNS *answered*. It's running, it's reachable, it
    took the query and it returned `NXDOMAIN`. The resolver is per
    container, but the zone it serves is per *network*: it knows the
    members of the networks this container is attached to and nothing else.
    `net-alpha` is not a name that exists in `net-gamma`'s universe.

22. **Predict**: fine, DNS is scoped. But both containers sit on bridges in
    the same kernel, and `net-gamma` has a default gateway. Send packets to
    `172.20.0.2` directly. Refused? Timeout? Unreachable?
23. **B**:

    ```sh
    ping -c 3 -w 6 172.20.0.2
    wget -O- http://172.20.0.2/
    ```

24. **Observe**:

    ```
    PING 172.20.0.2 (172.20.0.2): 56 data bytes

    --- 172.20.0.2 ping statistics ---
    3 packets transmitted, 0 packets received, 100% packet loss
    ```

    ```
    Connecting to 172.20.0.2 (172.20.0.2:80)
    ```

    ...and it sits there. Forever. That `wget` never returns; kill it with
    Ctrl-C, or re-run it as `wget -O- -T 5 http://172.20.0.2/` to get a
    `wget: download timed out` after five seconds.

    Compare with step 9's instant `Connection refused` from the *same*
    nginx serving the *same* port. Not a rejection — a silent drop.
    Something between the two bridges is swallowing the packets and
    answering nothing, which is precisely the signature of a firewall rule
    (Docker's `DOCKER-ISOLATION-STAGE-1`/`-2` iptables chains, which exist
    to drop exactly this traffic). The isolation isn't a missing wire; the
    route exists, the packets are emitted, and a rule kills them in flight.

### Part 4 — attach a running container to a second network

What this part tests: that membership is live state, not something baked
in at `docker run` time. Per the mental model, user-defined bridges accept
attach and detach on running containers — and if the DNS zone really is
just "the members of the networks I'm on", then a name should start
resolving the instant membership changes, with no restart.

25. **B** (`net-gamma`): establish the baseline and note the exact process:

    ```sh
    getent hosts net-beta ; echo "exit=$?"
    ```

    ```
    exit=2
    ```

    From your **host** shell:

    ```bash
    docker inspect -f '{{.State.StartedAt}} pid={{.State.Pid}}' net-beta
    ```

    ```
    2026-07-22T15:55:34.734351Z pid=15777
    ```

26. **Predict**: attach the *running* `net-beta` to `net-green`. Does it
    need a restart before `net-gamma` can find it? Does the container's PID
    change?
27. **Host**: `docker network connect net-green net-beta`
28. **B**: immediately, with no restart of anything:

    ```sh
    getent hosts net-beta
    wget -qO- http://net-beta/ | grep title
    ```

29. **Observe**:

    ```
    172.21.0.3        net-beta  net-beta
    ```

    ```
    <title>Welcome to nginx!</title>
    ```

    And on the **host**, `docker inspect` again:

    ```
    2026-07-22T15:55:34.734351Z pid=15777
    ```

    Same start time, same PID. Nothing restarted; nginx never noticed. A
    new veth pair was pushed into the existing network namespace and the
    DNS zone was updated, live.

30. **A** (`net-alpha`) and **B** (`net-gamma`): ask the *same question*
    from both networks.

    ```sh
    getent hosts net-beta
    ```

    ```
    172.20.0.3        net-beta  net-beta      # asked from net-alpha (net-blue)
    172.21.0.3        net-beta  net-beta      # asked from net-gamma (net-green)
    ```

    One name, two answers, decided by which network the asker is on. A
    dual-homed container has an address per network, and Docker's DNS hands
    each side the address that is actually usable *from there*. Confirm it
    from the **host**: `docker exec net-beta ip -4 -o addr show`

    ```
    11: eth0    inet 172.20.0.3/16 brd 172.20.255.255 scope global eth0
    12: eth1    inet 172.21.0.3/16 brd 172.21.255.255 scope global eth1
    ```

31. **Predict**: `net-beta` now sits on both networks and can talk to both
    `net-alpha` and `net-gamma`. Can `net-gamma` now reach `net-alpha`
    through it?
32. **B**: `getent hosts net-alpha ; echo "exit=$?"` and
    `wget -O- -T 4 http://172.20.0.2/`
33. **Observe**:

    ```
    exit=2
    ```

    ```
    Connecting to 172.20.0.2 (172.20.0.2:80)
    wget: download timed out
    ```

    Unchanged. Being multi-homed makes `net-beta` reachable from both
    sides; it does not make it a *router*. There's no forwarding between
    its two interfaces and no transitive DNS. This is the shape of a real
    pattern: put your reverse proxy on `frontend` and `backend`, leave the
    database on `backend` only, and the database is unreachable from the
    frontend network without a single firewall rule of your own.

    (The symmetric move works too:
    `docker network disconnect net-green net-beta` and `net-gamma`'s
    `getent` goes back to `exit=2` immediately, with `eth1` gone from
    `net-beta`. Reconnect before continuing if you tried it.)

### Part 5 — published ports have nothing to do with this

What this part tests: the most common misconception in the topic — that a
container needs `-p 8080:80` before another container can reach port 80.
Everything above already disproves it (no `-p` anywhere), but it's worth
watching the mapping fail to help.

34. **Host**: start a container that *does* publish a port.

    ```bash
    docker run -d --name net-pub --network net-blue -p 39217:80 nginx:alpine
    docker port net-pub
    docker port net-alpha
    ```

    ```
    80/tcp -> 0.0.0.0:39217
    80/tcp -> [::]:39217
    ```

    (and nothing at all for `net-alpha`, which has published nothing and
    has been happily serving Part 1's requests regardless).

35. **Predict**: from `net-alpha`, on the same network, which works —
    `http://net-pub/` (port 80) or `http://net-pub:39217/`?
36. **A**: both.

    ```sh
    wget -qO- http://net-pub/ | grep title
    wget -O- -T 4 http://net-pub:39217/
    ```

37. **Observe**:

    ```
    <title>Welcome to nginx!</title>
    ```

    ```
    Connecting to net-pub:39217 (172.20.0.4:39217)
    wget: can't connect to remote host (172.20.0.4): Connection refused
    ```

    Backwards from most people's mental model, and the refusal is the
    proof: **port 39217 does not exist inside the container.** `-p` creates
    a DNAT rule on the *host* that rewrites host:39217 → container:80. From
    inside the container network there is no 39217 to connect to, only the
    80 that nginx actually binds. Container-to-container traffic never
    touches that rule.

38. **Predict**: `net-pub` publishes a port to the world. Does that make it
    reachable from `net-gamma` on the other network?
39. **B**: `wget -O- -T 4 http://net-pub/` and
    `wget -O- -T 4 http://172.20.0.4:39217/`
40. **Observe**:

    ```
    wget: bad address 'net-pub'
    ```

    ```
    Connecting to 172.20.0.4:39217 (172.20.0.4:39217)
    wget: download timed out
    ```

    Publishing changed nothing about cross-network access. It exposes the
    container to *the host's* network stack, and `net-gamma` is not the
    host. Curl the host and it's plainly there —
    `curl -o /dev/null -w '%{http_code}' http://localhost:39217/` returns
    `200`.

41. The exception that proves it: from **B**, go out to the host and come
    back in.

    ```sh
    wget -qO- http://host.docker.internal:39217/ | grep title
    ```

    ```
    <title>Welcome to nginx!</title>
    ```

    `net-gamma` just reached a container it cannot reach — by leaving the
    container network entirely, arriving at the host's published port, and
    being DNAT'd back in. The isolation is between the two bridges, and
    routing around it via the host is a real (and frequently accidental)
    hole.

## What you should see

On one user-defined network: `nameserver 127.0.0.11` in `/etc/resolv.conf`,
`getent hosts net-beta` → `172.20.0.3`, and 0% packet loss. On the default
bridge: no embedded resolver at all, `ping: bad address 'net-legacy2'` —
and yet `ping 172.17.0.6` at the same 0.1 ms, the two questions visibly
diverging. Across two networks: an embedded resolver that is running and
answers `NXDOMAIN`, and a `wget` that connects to nothing forever rather
than being refused. Then `docker network connect` on a running container
making its name resolve immediately — to `172.21.0.3` from one network and
`172.20.0.3` from the other, same name, same instant, same container, PID
unchanged. And a published `-p 39217:80` that is `Connection refused` from
inside the container's own network.

## Why

Docker's embedded DNS server is a per-container process reachable at
`127.0.0.11` inside that container's network namespace, and the records it
serves are derived from one thing: the set of endpoints on the networks
that container is attached to. That's why the resolver can be present,
healthy, and still return `NXDOMAIN` — it isn't broken, it's answering
correctly for a zone that doesn't contain the name you asked for. It also
explains the two different answers for `net-beta` in Part 4: the zone is
built per network, so a container on two networks has two records, and each
asker is told the address reachable from where it stands. And it explains
why the default bridge has none of this — it predates the feature and is
kept for compatibility, so containers there fall back to the host's
resolver, which has no idea what a container is.

The packet side is a separate mechanism with the same root cause. Each
user-defined network is a Linux bridge; a container's veth lands in exactly
one bridge per network it joins. Traffic to another bridge's subnet does
get routed — the container emits it, the gateway sees it — and is then
dropped by Docker's `DOCKER-ISOLATION-STAGE-1`/`-2` iptables chains, which
exist for precisely this purpose. Dropped, not rejected, which is why you
get a hang rather than a `Connection refused`. That distinction is the
whole diagnostic value of Part 3: refused means your networking works and
your service doesn't; timeout means the packets never arrived.

Publishing a port is a third, unrelated mechanism: a DNAT rule in the
host's `nat` table mapping a host port to a container's IP and port. It is
about the boundary between the host and the container world, and has
nothing to say about traffic that is already inside that world. Ports are
open between containers on a shared network by default; `-p` is how you let
your *laptop* in, and nothing else.

Which is the actual lesson: on a single Docker host, isolation is not a
consequence of topology. Every container is a few virtual hops from every
other one and always was. What separates them is bookkeeping — a DNS zone
and a chain of iptables rules, both derived from a membership list you
control with `docker network connect` and `disconnect`, live, on running
containers.

## Go deeper

- **Network aliases.** `docker run -d --name net-delta --network net-blue
  --network-alias db --network-alias cache nginx:alpine`, then from
  `net-alpha` ask for `db`, `cache` and `net-delta` — all three return
  `172.20.0.5`. The alias is per *network attachment*, not per container,
  so the same container can be `db` on one network and something else on
  another. This is how blue/green cutovers work without touching client
  config: attach the new container under the alias, detach the old one.
- **Total isolation.** `docker run -d --name net-solo --network none
  nginx:alpine`, then `docker exec net-solo ip -o addr show` — only `lo`.
  Reaching for a container IP gives an instant
  `wget: can't connect to remote host (172.20.0.2): Network unreachable` —
  the fourth failure signature from the mental model, and a different one
  from Part 3's timeout: here the container's own kernel has no route and
  never puts a packet on any wire.
- **Watch the drop happen.** On a Linux host (not macOS), `sudo iptables -L
  DOCKER-ISOLATION-STAGE-2 -v -n` while re-running Part 3's `wget`, and
  watch the DROP rule's packet counter tick up. That counter is the
  isolation boundary, in one number.
- **`--internal` networks.** `docker network create --internal net-vault`
  builds a bridge with no route to the outside: containers on it can talk
  to each other and to nothing else, not even the internet. Compare
  `wget -O- -T 5 http://example.com/` from a container there versus one on
  `net-blue`. This is the "database subnet" pattern done properly.
- **What `docker compose` is doing.** Bring up any compose project and run
  `docker network ls` — you'll find a `<project>_default` user-defined
  bridge. Everything in Parts 1 and 4 is what makes `postgres:5432` resolve
  from your app container. Compose is a thin wrapper over exactly these
  commands.
- Docker's own reference for the pieces above:
  https://docs.docker.com/engine/network/drivers/bridge/ — in particular
  the "differences between user-defined bridges and the default bridge"
  section, which lists the DNS behavior you just watched.

## Cleanup

Remove only what this exercise created, by name:

```bash
docker rm -f net-alpha net-beta net-gamma net-pub net-legacy1 net-legacy2 net-delta net-solo
docker network rm net-blue net-green
```

(`net-delta` and `net-solo` only exist if you did the "Go deeper" steps.
Never `docker network prune` here — it would take out networks belonging
to other projects on your machine.)
