Docker gives every container its own network stack, connects containers through virtual networks, and maps ports to the host. Understanding a few concepts saves hours of “why can’t my app reach the database?” debugging.
The network drivers
docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
b2c3d4e5f6a7 host host local
c3d4e5f6a7b8 none null local
| Driver | What it does |
|---|---|
bridge |
Private virtual network on the host; containers get internal IPs (172.17.0.0/16 by default) and reach the internet through NAT |
host |
No isolation; the container uses the host’s network stack directly |
none |
No networking at all |
overlay |
Spans several hosts (Docker Swarm) |
macvlan / ipvlan |
Containers get addresses on the physical network |
Default bridge vs user-defined bridge
Containers started without --network join the default bridge network. On it, containers can reach each other only by IP — there’s no name resolution.
Create your own bridge network and containers can find each other by name, through Docker’s built-in DNS:
docker network create appnet
docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:17
docker run -d --name app --network appnet -e DATABASE_HOST=db myapp:1.0
The app connects to db:5432. User-defined networks also isolate groups of containers from each other. Always use them — Docker Compose does this automatically, creating a network per project where every service is reachable by its service name.
Publishing ports
Containers on a bridge network aren’t reachable from outside the host until you publish a port:
docker run -d -p 8080:80 nginx # host 0.0.0.0:8080 → container :80
docker run -d -p 127.0.0.1:8080:80 nginx # localhost only
docker run -d -p 8443:443/tcp -p 53:53/udp image
docker port <container> # show mappings
Two security points:
- Published ports bypass UFW/firewalld. Docker inserts iptables rules ahead of the host firewall.
-p 8080:80is reachable from the internet even if UFW denies 8080. - Only publish what must be public. Databases and internal services don’t need
-pat all — other containers on the same network reach them directly.
The recommended pattern: publish app containers on 127.0.0.1, and let a reverse proxy on ports 80/443 forward traffic — see Nginx reverse proxy for Docker containers.
To filter traffic to published ports, use the DOCKER-USER iptables chain, which Docker leaves for your rules.
Container to host
From inside a container, localhost is the container itself. To reach a service on the host (e.g. a database installed on the host):
docker run --add-host=host.docker.internal:host-gateway myapp
The container can now use host.docker.internal. The host service must listen on an address reachable from the bridge (the Docker gateway, typically 172.17.0.1, or 0.0.0.0 with a firewall rule) — not just 127.0.0.1.
Host networking
docker run -d --network host nginx
The container shares the host’s interfaces: no NAT, no port mapping, slightly better performance, and port conflicts with host services. Useful for network tools and some high-throughput or UDP-heavy apps. Ports aren’t isolated, so use it deliberately.
Connecting containers to several networks
A common layout separates the front end from the database:
services:
proxy:
image: nginx:stable
ports: ["80:80", "443:443"]
networks: [frontend]
app:
image: myapp:1.0
networks: [frontend, backend]
db:
image: postgres:17
networks: [backend]
networks:
frontend:
backend:
internal: true # no outbound internet access
The proxy can’t reach the database at all; only the app talks to both. internal: true blocks the backend network from reaching the internet.
Attach a running container to an extra network:
docker network connect backend app
docker network disconnect backend app
Inspecting networks
docker network inspect appnet # subnet, gateway, attached containers
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' app
Debugging connectivity
Use a throwaway container with network tools on the same network:
docker run --rm -it --network appnet nicolaka/netshoot
# inside:
dig db
nc -zv db 5432
curl -I http://app:3000
Checklist when containers can’t talk:
- Are both on the same user-defined network? (
docker network inspect) - Using the service/container name, not
localhost? - Using the container port, not the published host port?
- Is the service listening on
0.0.0.0inside its container, not127.0.0.1? - Containers have no internet? Check
sysctl net.ipv4.ip_forwardis1and that a firewall script hasn’t flushed Docker’s rules (restart Docker to recreate them).
Address pool conflicts
Docker’s default subnets (172.17.0.0/16 and up) can clash with VPN or office networks. Set different pools in /etc/docker/daemon.json:
{
"default-address-pools": [{ "base": "10.200.0.0/16", "size": 24 }]
}
Restart Docker; existing networks keep their old subnets until recreated.
Related
Docker networking works the same on any KVM server; our Docker VPS plans give you full control of the host firewall and kernel networking settings.