Sitemap

“It’s Always DNS” Is Wrong. In Docker, It’s Always the Default Bridge.

Why your container can’t find the database, what actually does (nothing), and the port-publishing default that quietly changed under everyone in Docker 28.

7 min readOct 15, 2026

--

There’s a support conversation I have had, by my rough count, somewhere north of two hundred times. It goes like this:

“My app can’t connect to Postgres.”
“How is it addressing it?”
“
."
"How did you start them?"
"
and "

And there it is. Both containers are on the default bridge network, where container names do not resolve, and no amount of staring at connection strings will fix it. The fix is one flag. The understanding is worth more than the flag, so let’s do the understanding.

The default bridge is a legacy artifact, and you should treat it like one

When you without , your container joins a pre-existing network called . It gets an IP, it can reach the internet, it can reach other containers on that same bridge by IP. What it cannot do is resolve their names.

The docs are unambiguous about this: “Containers on the default bridge network can only access each other by IP addresses, unless you use the option." And about : it's legacy, and "These links need to be created in both directions, so you can see this gets complex with more than two containers which need to communicate."

Create your own network and the behaviour changes entirely — “User-defined bridges provide automatic DNS resolution between containers.”

docker network create appnet
docker run -d --name db --network appnet postgres:17
docker run -d --name api --network appnet myapi
# inside api: psql -h db ← works

That’s the whole fix. One and a on each container.

My actual opinion: never put a container on the default bridge on purpose. Not “prefer user-defined networks” — never. The default bridge has a second problem that’s worse than the DNS thing, and the docs say it out loud: “All containers without a specified, are attached to the default bridge network. This can be a risk, as unrelated stacks/services/containers are then able to communicate." Your side project's Redis and your client's staging database, sitting on the same L2 segment, because you forgot a flag. The docs call the default bridge "considered a legacy detail of Docker." Believe them.

(While I was rereading that page I found a limit I genuinely did not know: “Bridge networks become unstable and inter-container communications may break when 1000 containers or more connect to a single network.” A kernel limitation, not a Docker one. You will probably never hit it.)

Where the name resolution actually happens

Not in . Not in some magic Docker-aware libc. There's a real DNS server, at a real address, and you can query it.

Docker runs an embedded DNS resolver at and injects it into for containers on user-defined networks. Containers on the default bridge instead "receive a copy of" the host's — which is exactly why names don't resolve there. There is no resolver in the path to resolve them.

Two details worth pocketing:

  • There’s no IPv6 version. Per the docs: “There is no IPv6 equivalent; the IPv4 address works even in IPv6-only containers.” If you’ve ever hardcoded a nameserver in an app config and broken container DNS, this is the address you needed.
  • On user-defined networks the embedded server “queries upstream servers in order and stops after a successful response or an NXDOMAIN response” — subtly different failure semantics than your resolver library’s own strategy. It matters when a search-domain-heavy corporate DNS setup is in play.

And you can add extra names: , or , gives a container additional resolvable names on that network. This is how you run and have it also answer to during a migration, without touching a single connection string.

from inside a container, and look at the line. Seeing answer is the moment the whole thing stops being magic.

Networks are real isolation, and a container can sit on several

Two user-defined networks are genuinely separate. A container on cannot reach a container on , by name or by IP. That's not a firewall rule you configured; that's just how it is.

The thing people underuse is that membership isn’t exclusive:

docker network create frontend
docker network create --internal backend
docker run -d --name db  --network backend  postgres:17
docker run -d --name api --network backend myapi
docker network connect frontend api
docker run -d --name proxy --network frontend -p 443:443 nginx

Read that topology. The API sits on both networks. The database sits on exactly one, and that one is , which means no outbound route to the wider world at all. There is no host port anywhere near the database, and there is no path from the proxy to it. You just built a DMZ with four commands and zero iptables.

Get Vin’s stories in your inbox

Join Medium for free to get updates from this writer.

I bring this up because I keep seeing single-network compose stacks where every service can reach every other service, and then a lot of anxiety about securing them. Segment first. It’s free.

Publish vs. expose: one of them does nothing

This is the other half of the perennial confusion, and it’s worth being blunt.

in a Dockerfile opens nothing. It is metadata. A comment with a schema. Its only functional effect is that (capital P) will pick ephemeral host ports for the exposed ones. That's it. An image with no line is just as reachable as one with it.

is the thing that does something: it creates a host→container forward, so traffic arriving at your machine's port 8086 reaches the container's port 80.

The corollary that saves the most time in practice: container-to-container traffic needs no published ports whatsoever. Two containers on a shared network talk directly on the container port. If your compose file publishes Postgres on 5432 so that “the app can reach it,” delete that line — the app was never using it, and you’ve been exposing your database to the LAN for nothing. Per the docs, published ports are about reaching things from outside the host (and from containers on other bridge networks): “Use the or flag to make a port available outside the host."

Where gets genuinely dangerous

binds . Everyone on your network can reach it. binds loopback, so only your own machine can. That difference is a coffee-shop-wifi-shaped hole in a lot of dev setups.

Worse, on Linux: Docker’s published ports bypass your host firewall. Not “can conflict with” — bypass. The docs, verbatim: “When you publish a container’s ports using Docker, traffic to and from that container gets diverted before it goes through the ufw firewall settings.” The mechanism is that Docker does its work in the table, so "packets are diverted before it reaches the INPUT and OUTPUT chains that ufw uses."

So plus equals an open database, and a firewall config that lies to you when you audit it. If you take one operational habit from this post: bind published ports to unless you have specifically decided otherwise, and put a real reverse proxy in front of anything that needs to be public.

The default that changed under all of us (Docker 28)

Here’s my favourite kind of fact — the kind that makes half the tutorials on the internet wrong.

Before Docker Engine 28, on a Linux host, a container’s IP could be reachable from other machines on your LAN even for ports you never published. Not by accident of Docker, exactly, but as a consequence of IP forwarding plus a permissive chain plus another host on the subnet having a route to the container network. Unpublished didn't mean unreachable; it meant "not reachable via the host's port, but possibly reachable at the container's own IP if the network conditions line up." In a multi-tenant LAN, anyone on the same RFC1918 subnet who could guess a container IP could knock on ports you thought were private.

Docker 28 changed the default: unsolicited inbound traffic to a container’s IP is now dropped unless that port was explicitly published. Local connections from the Docker host itself still work. Published ports still work exactly as before. And if you were depending on the old behaviour, you now have to opt back into it (, or a network).

Two things I want you to take from this. First: if you read a networking blog post written before 2025, its threat model is out of date. Second: this is the third Docker default I’ve had to re-learn. Check the current docs before you trust the shape of your own memory. I write these posts partly to force myself to do exactly that.

About on a Mac

Old advice: “host networking doesn’t work on Docker Desktop, it’s Linux-only, stop trying.”

That advice is now stale, and I had to correct my own notes while writing this. Docker Desktop 4.34 and later does support host networking on Mac and Windows — but it’s opt-in (Settings → Resources → Network → “Enable host networking”), and it comes with real caveats worth quoting:

  • “The host network feature of Docker Desktop works on layer 4. This means that unlike with Docker on Linux, network protocols that operate below TCP or UDP are not supported.” So no raw sockets, no ICMP games, no -style tricks that assume L2/L3 parity with Linux.
  • “Processes inside the container cannot bind to the IP addresses of the host.”
  • It doesn’t work with Enhanced Container Isolation, which the docs justify neatly: those two features “contradict each other.”
  • Linux containers only.

So: it exists, it’s usable, and it is not the same primitive as on Linux. If you're following a Linux tutorial that leans on host networking for something below L4, it'll still let you down — just for a different reason than before.

The five-line summary

  • User-defined network, always. The default bridge has no DNS and no isolation, and it’s legacy.
  • Names resolve via the embedded DNS at , and only on user-defined networks.
  • is a comment. is the mechanism. Container-to-container needs neither.
  • Bind published ports to loopback by default; on Linux they route around ufw entirely.
  • Segment with multiple networks and . It costs nothing and removes whole classes of exposure.

--

--