A bridge is a private network on the host
Each bridge network is a virtual switch on the Docker host. Containers on the same bridge can talk to each other; containers on another bridge cannot see them. An IP such as 172.17.0.2 only means something inside the host; other machines on the LAN cannot reach it directly.
flowchart LR
C["Client"] -- "203.0.113.10:8080" --> NAT["NAT -p 8080:80"]
subgraph HOST["Docker host"]
NAT --> W["web 172.18.0.2:80 (app-net)"]
D["db 172.19.0.2:5432 (db-net)"]
end
Publishing ports
| Syntax | Result |
|---|---|
-p 8080:80 |
Every host interface, port 8080 → container 80 |
-p 127.0.0.1:8080:80 |
Only the host itself (localhost) can connect |
-p 80 |
Docker picks a random host port |
docker port web |
Show the current mappings of container web |
Two containers cannot publish the same host port: the second one fails with port is already allocated. Change the host port (-p 8081:80) and keep the container port.
Default bridge vs user-defined bridge
Default bridge |
User-defined bridge | |
|---|---|---|
| Reach others by container name | No (IP only, or legacy --link) |
Yes, automatic DNS |
| Isolation | Every container without a network shares it | Only containers you attach |
| Attach/detach while running | Limited | docker network connect/disconnect |
| Own configuration | Shared for the whole host | Subnet and options per network |
So for a multi-container app, always create a user-defined bridge instead of using the default bridge.
How publishing works underneath
On Linux, Docker implements -p with NAT rules in iptables (or nftables): traffic arriving on the host port is rewritten to the container's IP and port, and outgoing traffic from containers is masqueraded behind the host's address. sudo iptables -t nat -L DOCKER -n shows the rules Docker created. -P (--publish-all) publishes every port listed by EXPOSE to random host ports.
The firewall trap
Because Docker inserts its own rules before most host firewall rules, a published port can be reachable from the internet even when ufw appears to block it. If a service should only be local, publish it on 127.0.0.1 (-p 127.0.0.1:5432:5432), or add rules to the DOCKER-USER chain, which Docker leaves for administrators. In the cloud, also check the VM's security group.
The app must listen on all interfaces
A frequent mistake is an application that listens on 127.0.0.1 inside the container. The port mapping then forwards traffic to the container's network interface, where nothing is listening, and the client gets "connection reset". Configure the app to listen on 0.0.0.0 (for example flask run --host=0.0.0.0).
Checking connectivity
| Step | Command |
|---|---|
| Is the container running and the port mapped? | docker ps, docker port web |
| Is the app listening inside? | docker exec web ss -lnt (if the image has ss) |
| Can the host reach it? | curl -I http://localhost:8080 |
| What network and IP does it have? | docker inspect -f '{{json .NetworkSettings.Networks}}' web |
IPv6 and outbound traffic
Bridges are IPv4 by default; IPv6 can be enabled per network with --ipv6. Outbound connections from containers work without any -p: publishing is only needed for traffic coming into the container from outside the host.