How Docker Networking Works (Bridges, Container Names, Shared Networks and macvlan)

Most Docker confusion is network confusion: the reverse proxy can't reach an app, two compose stacks can't talk, Home Assistant can't find devices, "localhost" means the wrong machine. Once you can picture the three or four network types Docker uses, every one of those has an obvious fix.
Quick reference
| Situation | Answer |
|---|---|
| Containers in the same compose file | reach each other by service name: http://db:3306 |
| Containers in different compose files | put both on a shared network: docker network create proxy |
| Container needs the host's services | host.docker.internal + extra_hosts: ["host.docker.internal:host-gateway"] |
| App must see LAN broadcasts (Home Assistant, Plex discovery) | network_mode: host |
| Container needs its own LAN IP (Pi-hole DHCP, AdGuard) | a macvlan network |
| See what's where | docker network ls · docker network inspect name |
1. The default: a bridge per compose file
Each compose project gets its own private network (projectname_default). Inside it, every service is reachable by its service name — Docker runs a DNS server for the network. That's why DB_HOST: db works and why nothing outside the stack can reach db at all. Ports only become visible to the LAN when you publish them with ports:.
127.0.0.1 inside a container is that container, not the server. That's the single most common mistake.
2. Two stacks that need to talk
The reverse proxy in ~/docker/nginx and the app in ~/docker/app are on different bridges and can't see each other by name. Give them a shared network:
docker network create proxy
In both compose files:
services:
app:
networks: [default, proxy]
networks:
proxy:
external: true
Now the proxy reaches http://app:3000 by name, with no published port on the app at all — the cleanest way to run a reverse proxy (tutorials 28, 57, 58). The default entry keeps the app's own private network for its database.
3. Reaching the host
For a container that must talk to something on the server itself — Ollama installed on the host (tutorial 78), a host MariaDB, a USB device server:
extra_hosts:
- "host.docker.internal:host-gateway"
and use http://host.docker.internal:11434. The host service must listen on 0.0.0.0 or on Docker's bridge address (172.17.0.1), not only on 127.0.0.1.
4. Host networking
network_mode: host
The container shares the server's network stack: no private network, no ports: (it just listens on the host's ports), sees broadcasts and multicast. Needed for Home Assistant, Netdata, anything doing device discovery. The cost: no isolation, and port clashes are yours to manage.
5. macvlan: a container with its own LAN address
Sometimes a container should look like a separate device on the network — Pi-hole acting as DHCP server, a second DNS, a camera NVR the cameras must reach directly.
docker network create -d macvlan \
--subnet=192.168.1.0/24 --gateway=192.168.1.1 \
--ip-range=192.168.1.240/28 \
-o parent=enp3s0 lan
services:
pihole:
networks:
lan:
ipv4_address: 192.168.1.241
networks:
lan:
external: true
The container now has 192.168.1.241, answers ARP, gets its own place in the router's device list, and needs no ports:. Two catches: the host can't talk to macvlan containers by default (a known kernel limitation — a small macvlan shim interface on the host fixes it), and Wi-Fi interfaces usually can't be parents. Reserve the IP range on the router so DHCP doesn't hand those addresses out.
6. Looking around
docker network ls
docker network inspect proxy # who's on it, with IPs
docker compose exec app getent hosts db
docker compose exec app wget -qO- http://db:3306 # or curl, if the image has it
inspect is the fastest way to answer "are these two actually on the same network?"
Firewall reminder
Published ports bypass UFW (tutorial 6). Prefer the shared-network pattern with no published ports, or bind to 127.0.0.1: when a port must exist. The ufw-docker tool (tutorial 105) closes the gap properly.
Troubleshooting
"Name does not resolve" between containers. Different networks. Shared network as in step 2, and check the name is the service name, not container_name (both usually work, but the service name always does).
502 from the proxy to an app that works on its port. Proxy is reaching 127.0.0.1 inside its own container. Use the app's service name on a shared network, or the server's LAN IP.
A container can't reach the internet. Docker's bridge NAT broken after a firewall change: sudo systemctl restart docker. Or UFW's default forward policy — DEFAULT_FORWARD_POLICY="ACCEPT" in /etc/default/ufw is what Docker expects.
IP conflicts after adding macvlan. The router handed out an address in your container range. Reserve the range.
Wrapping up
Same stack: by name. Different stacks: a shared network. Host services: host.docker.internal. Discovery: host mode. A real LAN presence: macvlan. With those five pictures, docker network inspect turns every "can't connect" into a two-minute fix.
