The first time I ran two containers on the same host and expected them to see
each other, I got "connection refused" for an hour. Both were up, both worked
alone, and pinging one by name from the other did nothing. In Docker, name
resolution is not automatic. It depends on which network the containers are
on.
I have deployed 30-plus Nextcloud, Jitsi, Matrix, and Moodle stacks for
clients, and almost every networking ticket traces back to four problems:
containers in one project that cannot talk, a service that answers curl
locally but not from a laptop, a database port open to the internet even
though UFW blocks it, and host mode copied from a tutorial without knowing
what it gives up.
How Docker Networking Actually Works
Every container gets its own network namespace, Linux's way of giving a
process its own interfaces, routes, and IP address. Docker links that
namespace to the host with a virtual ethernet (veth) pair: one end inside the
container, the other plugged into a bridge on the host, like a virtual switch.
That bridge is docker0 by default. Outbound traffic is translated by NAT to
the host's IP, the same trick your home router uses, and publishing a port
with -p sets up the reverse rule. That one mechanism explains most of the
surprises below.
Docker's Network Drivers, Compared
Docker Engine 29.x ships six network drivers; five matter in practice.
| Driver | What it does | Use it when |
|---|---|---|
| bridge | Private network on the host, NAT to reach outside | Default choice for |
almost every single-host setup
host| Container shares the host's namespace, no NAT| The app needs raw
throughput or binds ports dynamically
overlay| VXLAN network spanning multiple hosts| You run Swarm across more than
one server
macvlan| Own MAC address and a real IP on your LAN| The container must look
like a physical device
ipvlan| Containers share the parent's MAC, separate IPs| Your switch limits
MACs per port
none| No networking at all| Rare, for isolated batch jobs
New to containers? My Docker for beginners
guide covers the earlier mistakes.
The Default Bridge Is Not Good Enough
Installing Docker creates a default bridge called docker0 on subnet
172.17.0.0/16 with gateway 172.17.0.1. Every container started without
--network lands there.
The catch: it has no DNS between containers, so you cannot reach one by name.
The old answer, the --link flag, was deprecated years ago and should not
appear in new work; Docker's docs now treat the default bridge as legacy.
Create your own instead:
docker network create --driver bridge --subnet 172.20.0.0/16 mynet
docker run -d --name app --network mynet nginx
docker run -d --name db --network mynet postgres:16
Any network you create is a user-defined network, and Docker runs an embedded
DNS server at 127.0.0.11 inside every container attached to it. It maps
container names and aliases to their current IPs and forwards everything else
upstream. That is the biggest reason to abandon the default bridge.
docker exec -it app ping -c2 db
docker exec -it app nslookup db 127.0.0.11
Containers Talking to Containers (Compose)
This is why Compose usually just works. docker compose up creates a user-
defined bridge per project named <project>_default, so embedded DNS is live
immediately and every service is reachable by its service name.
If services cannot reach each other, check two things: they live in the same
Compose file and start together, and they connect by service name rather than
localhost. A web app should talk to db:5432, not 127.0.0.1:5432, because
inside a container localhost is the container itself.
One pattern I use often: give the database a network with no way out.
services:
web:
image: myapp:latest
networks: [frontend, backend]
ports:
- "127.0.0.1:8080:80"
db:
image: postgres:16
networks: [backend]
networks:
frontend:
backend:
internal: true
internal: true means containers on backend cannot reach the internet and
nothing outside can reach them. The database can talk to web; nothing can
talk to the database directly.
Ports, UFW, and the Security Mistake Everyone Makes
This one surprises experienced admins. You configure UFW, block 5432 from
outside, run docker run -p 5432:5432 postgres, and Postgres is still
reachable from the internet. UFW never saw the decision.
Publishing a port is DNAT, and Docker writes its own rules into the DOCKER
and DOCKER-USER iptables chains, ahead of your regular firewall rules. It is
documented, expected behavior rather than a bug.
Three fixes, simplest first:
- Bind to localhost:
-p 127.0.0.1:5432:5432. Nothing outside the host can reach it. Put a reverse proxy in front for anything public; my NGINX reverse proxy guide covers that. - Add rules to the
DOCKER-USERchain, evaluated before Docker's forwarding rules. - Use the
ufw-dockertool if you want UFW to stay in charge.
For anything holding real data I use option one on nearly every client server.
When to Use Host Mode (and When Not To)
With network_mode: host the container gives up its own network namespace and
shares the host's. No veth pair, no NAT, lower latency. Fair for Home
Assistant, some Pi-hole setups, and services where NAT overhead measurably
matters.
The cost: ports: is ignored and the app binds host ports directly, so two
host-mode containers cannot both want 8080, and you lose isolation. Default to
bridge; use host mode only for a measured reason, such as multicast device
discovery.
Giving a Container a Real LAN IP with Macvlan
Macvlan gives a container its own MAC address and a real IP on your LAN, so
your router sees it as a separate physical machine. That matters for Home
Assistant when it must see Z-Wave or Zigbee devices over multicast, and for
Pi-hole when every device should use it as their DNS server by IP.
docker network create -d macvlan \
--subnet=192.168.1.0/24 --gateway=192.168.1.1 \
-o parent=eth0 macnet
docker run -d --name pihole --network macnet --ip 192.168.1.50 pihole/pihole
The gotcha: the host cannot talk to its own macvlan containers by default,
because traffic from the parent interface back to a macvlan child on the same
interface is dropped by the kernel. The fix is a macvlan shim, a small host
interface bridged into the same network, created with ip link add. Skip it
and the container works from every device except the one running Docker.
IPvlan is similar, but containers share the parent's MAC, which helps where
your switch limits MACs per port. Overlay is the multi-host option, tunneling
traffic with VXLAN on UDP 4789, and rarely worth the complexity outside Swarm.
IPv6 is off by default and must be enabled in /etc/docker/daemon.json. And
on rootless Docker in 2026, networking now defaults to gvisor-tap-vsock
instead of slirp4netns, a slower path than rootful Docker.
Three Fixes for the Errors I See Most
-
A container name will not resolve. Compare networks with
docker network inspect mynet. Containers on the default bridge, or split across two Compose projects, will never resolve each other. Test from inside:docker exec -it app nslookup db 127.0.0.11. -
Connection refused although the process is running. Usually not a networking problem. Check the bind address inside the container with
netstat -tlnp. If the app listens on127.0.0.1instead of0.0.0.0, it only accepts connections from itself, and no port mapping can fix that. -
TLS handshakes hang over a VPN. Usually an MTU mismatch. Containers default to 1500 while WireGuard tunnels typically run at 1420. Set
"mtu": 1420in/etc/docker/daemon.jsonor per-network withdriver_opts, then restart the containers.
Frequently Asked Questions
Do I need a custom Docker network?
Yes, for anything beyond a standalone container. The default bridge has no DNS
between containers, so a user-defined bridge is the standard fix for name-
based service discovery.
What is 127.0.0.11 in Docker?
It is the embedded DNS server Docker runs inside containers on a user-defined
network. It resolves container names and aliases to their current IPs and
forwards other lookups upstream.
How do I stop a container port from being public?
Bind it to localhost, for example -p 127.0.0.1:8080:80, and put a reverse
proxy in front of anything that must be public. That avoids the trap where
Docker's iptables rules bypass UFW.
Does host networking make Docker faster?
It removes NAT and the veth pair, lowering latency and CPU overhead slightly,
but the difference is rarely noticeable for typical self-hosted apps. It also
disables port mapping and isolation, so it is a tradeoff, not a free upgrade.
Final Thoughts
Most Docker networking problems come down to one root cause: not knowing which
network a container is actually on, or assuming a port is private when Docker
has quietly made it public. Create your own networks, check bind addresses,
and most of this stops being mysterious.
Setting up Nextcloud, Jitsi, Matrix, or Moodle and want a second pair of eyes
on the networking before go-live? Feel free to reach out. This is what I fix
for clients every week.
Top comments (0)