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

Docker networking, finally explained: Why localhost is the wrong machine, and four ways to fix 'can't connect'.

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

SituationAnswer
Containers in the same compose filereach each other by service name: http://db:3306
Containers in different compose filesput both on a shared network: docker network create proxy
Container needs the host's serviceshost.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 wheredocker 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.

Call us