Why your Docker container isn't reachable
The symptoms all look identical from outside. Six causes account for nearly all of them, and each has a test that rules the others out.
Container networking has a small number of concepts and an enormous number of ways to get them slightly wrong. The symptoms all look the same from outside: the container is running, the logs look healthy, and nothing can reach it.
Almost every case is one of six things. Here they are, with the test that distinguishes them.
The four network modes
| Mode | What the container gets | Reachable from the LAN |
|---|---|---|
| bridge (default) | A private address on a virtual network inside the host | Only through published ports on the host's address |
| host | The host's network stack directly, no isolation | Yes, on whatever port it binds, no publishing needed |
| macvlan | Its own address on your real LAN, like a separate machine | Yes, directly, but see the caveat below |
| none | No networking at all | No |
Default bridge is the right choice most of the time. Host mode is the pragmatic answer for anything that needs broadcast or multicast traffic, which is why media servers, home automation hubs and anything doing device discovery so often want it: discovery protocols do not survive being NATed through a bridge.
Macvlan gives a container a real address on your network, which is lovely until you meet the caveat: the host cannot talk to its own macvlan containers. That is how the underlying mechanism works, not a bug. A container on macvlan is reachable from every device on your LAN except the machine it runs on, which produces a memorably confusing afternoon if you did not know. Working around it means an extra macvlan interface on the host.
Port publishing, and the direction people reverse
In -p 8080:80, the first number is the host and the second is the container. Host first, then container. Getting it backwards publishes the wrong thing and the error is not obvious, because something usually does start listening.
Two related details worth knowing:
Binding to a specific interface. -p 127.0.0.1:8080:80 publishes only on the host's loopback, so the service is reachable from the host and from nowhere else. This is a genuinely useful pattern when a reverse proxy on the same machine is the only thing that should reach the container, and a genuinely confusing one if you copied it from a guide without noticing.
Nothing published at all. A container in bridge mode with no published ports is reachable only from other containers on the same network. Plenty of compose files omit publishing on purpose because a proxy container handles ingress.
The fastest way to find out what is actually true: from a different device, open a TCP connect to the host's LAN address and the port you expect. If it opens, publishing works and your problem is inside the application. If it does not, publishing is the problem and there is no point reading application logs.
# From a phone on the same network, not from the host itself
https://netdebug.app/open/portconnect?target=192.168.1.50&port=8080&protocol=tcp
Test from a device that is not the host
TCP/UDP Connect tells you whether a published port is genuinely reachable from the network, and LAN Scan shows whether a macvlan container picked up the address you expected. Testing from the host itself hides exactly the problems you are looking for.
The localhost trap
This is the single most common cause, and the symptom is precise: the application works when you exec into the container and curl it, and fails from everywhere else.
Many applications bind to 127.0.0.1 by default, accepting connections only from the same machine. Inside a container, "the same machine" means that container and nothing else. Docker dutifully publishes the port, traffic arrives, and the application refuses it because it came from outside its own loopback.
The fix is in the application's configuration, not in Docker: bind to 0.0.0.0 instead. Depending on the software this is a config file setting, an environment variable, or a command line flag, and it is often phrased as "listen address" or "bind address".
The tell is worth memorising. Works inside the container, fails outside, published port present, no errors in the logs. That is always this.
Containers talking to each other
Two rules cover almost everything.
Use a user defined network, not the default bridge. Containers on a user defined network can resolve each other by container or service name. The default bridge has no such DNS, so name based connections fail there and people conclude container networking is broken when they have simply not created a network. Compose creates one for you, which is why this rarely bites in compose and frequently bites with plain run commands.
Use the service name and the container's own port. If your application connects to a database, the address is the service name and the port the database listens on inside the container, not localhost and not the published host port. localhost inside a container means that container, so an application looking for its database on localhost is looking inside itself.
Reaching the host from inside a container is the other direction of the same question. On Docker Desktop, host.docker.internal resolves to the host. On Linux it may need an explicit host entry, or you use the bridge gateway address, typically 172.17.0.1.
DNS inside a container
A container does not get its resolver from your network's DHCP. It gets it from the container runtime, which by default passes through the host's resolver or substitutes its own.
The consequence matters if you run internal DNS: the host can resolve nas.home.example.com and a container on the same machine cannot. Nothing is broken, the container is simply asking a different server. Fix it at the runtime or compose level by specifying the DNS servers explicitly.
On a host using systemd-resolved there is a further wrinkle: the host's resolver file may point at a loopback stub that means nothing inside a container, so the container inherits an address it cannot reach. Pointing containers at your real resolver address avoids the whole class of problem.
To confirm which side is at fault, query your resolver directly from another device with DNS Lookup and its custom server field. If the resolver answers correctly there, the container's configuration is the issue rather than the record.
Subnet collisions
Docker allocates private ranges for its networks, commonly starting at 172.17.0.0/16 and continuing through 172.18, 172.19 and upward as you create more.
If your LAN, your VPN or your employer's network uses a range inside 172.16.0.0/12, you get a collision. The container's routing table sends traffic for the overlapping range into the Docker network instead of out to the real one, and the symptom is that specific hosts become unreachable from containers while everything else works perfectly.
It is a particularly annoying failure because it is intermittent by nature: it depends on which networks Docker has created, which depends on the order you started things. A stack that works today breaks after a reboot creates networks in a different order.
The fix is to pin Docker's address pools to a range nothing else uses, in the daemon configuration. A subnet calculator is useful for picking a block and confirming it does not overlap anything you have. Pick something obscure, such as 10.201.0.0/16, rather than a default anyone else would also choose.
The firewall surprise
Worth ending on, because it catches people who did everything else right.
On Linux, Docker manages its own firewall rules to make port publishing work, and it inserts them ahead of the rules a simple firewall front end creates. The practical result: a published port can be reachable from your whole network even though your firewall says that port is blocked.
You configure a firewall, you check its status, it reports the port as denied, and a device on the other side of the house connects to it anyway.
This matters most for anything you assumed was private. A database published with -p 5432:5432 is on your LAN, firewall or not, and if your router ever forwards that port or you are testing from an untrusted network, it is further than that.
Two practical habits:
- Publish to loopback when only the host needs it.
-p 127.0.0.1:5432:5432keeps a database available to a local application and nothing else. - Verify from outside rather than trusting the firewall's own report. A port scan from another device shows what is genuinely reachable, and an external port check shows whether anything is exposed to the internet. Both answer the question the firewall status command does not.
Saving a scan of your container host's common ports as an automation and running it after stack changes is a cheap habit that catches accidental exposure the day it happens rather than months later.
The short version
Host first, container second, in the port mapping. If it works inside the container and nowhere else, the application is bound to localhost and needs 0.0.0.0. Use a user defined network so containers can find each other by name, and remember that a container's resolver comes from the runtime rather than your DHCP. Pin Docker's address pools away from anything your LAN or VPN uses. And test from a different device, because the host is the one machine that cannot show you these problems.
Frequently asked questions
Which number is the host port in a Docker port mapping?
The first one. In -p 8080:80 the host listens on 8080 and forwards to port 80 inside the container. Reversing them publishes the wrong thing, and the mistake is not obvious because something usually still starts listening.
My app works inside the container but not from outside. Why?
The application is bound to 127.0.0.1, which inside a container means that container alone. Docker publishes the port and traffic arrives, but the application refuses connections that did not come from its own loopback. Configure it to bind 0.0.0.0 instead.
Why can my containers not reach each other by name?
They are probably on the default bridge network, which provides no DNS between containers. Create a user defined network, where containers resolve each other by container or service name. Compose does this automatically, which is why the problem mostly appears with plain run commands.
Why can the host not reach a macvlan container?
By design. The macvlan mechanism does not allow the host to communicate with containers attached to it through the same physical interface. The container is reachable from every other device on the LAN except the machine running it. Working around it needs a separate macvlan interface on the host.
Why can a container not resolve my internal DNS names?
Containers get their resolver from the container runtime, not from your network's DHCP, so a container can be asking a completely different DNS server than its host. Set the DNS servers explicitly at the runtime or compose level.
What causes Docker subnet conflicts?
Docker allocates private ranges starting around 172.17.0.0/16. If your LAN, VPN or company network uses a range inside 172.16.0.0/12, traffic for those addresses is routed into the Docker network instead. Pin Docker's address pools to a block nothing else uses.
Does Docker bypass my firewall?
On Linux it effectively can. Docker inserts its own rules ahead of those created by simple firewall front ends, so a published port can be reachable across your network even though the firewall reports it as blocked. Publish to 127.0.0.1 when only the host needs access, and verify from another device.
NetDebug Toolkit