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:

  1. Published ports bypass UFW/firewalld. Docker inserts iptables rules ahead of the host firewall. -p 8080:80 is reachable from the internet even if UFW denies 8080.
  2. Only publish what must be public. Databases and internal services don’t need -p at 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:

  1. Are both on the same user-defined network? (docker network inspect)
  2. Using the service/container name, not localhost?
  3. Using the container port, not the published host port?
  4. Is the service listening on 0.0.0.0 inside its container, not 127.0.0.1?
  5. Containers have no internet? Check sysctl net.ipv4.ip_forward is 1 and 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.

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.

  • Docker
  • Networking
  • Containers