Every Docker container gets its networking from a network driver. The driver decides whether a container has its own isolated network stack, shares the host's, or joins a network that spans several servers. In this tutorial you will inspect the default networks on Ubuntu 24.04, create a user-defined bridge network where containers find each other by name, publish ports without exposing them by accident, and try the host, none and overlay drivers so you know when each one fits.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges, added to the docker group.
  • Docker Engine installed from Docker's official repository.
  • For the overlay section only: a second server running Docker that can reach the first one over the network.

Docker network drivers at a glance

DriverScopeIsolationTypical use
bridgeSingle hostOwn network namespace, NAT to the outsideDefault for most containers and Compose projects
hostSingle hostNone: shares the host network stackMaximum network performance, apps that need many ports
noneSingle hostLoopback onlyBatch jobs that must not touch the network
overlayMulti-hostVXLAN tunnel between Docker hostsDocker Swarm services across several servers
macvlan / ipvlanSingle hostContainer gets an address on the physical LANLegacy apps that must appear as a physical device

Step 1 - Inspecting the default networks

Docker creates three networks when it is installed:

docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
3c1f2a9b8d7e   bridge    bridge    local
9a8b7c6d5e4f   host      host      local
1a2b3c4d5e6f   none      null      local

The bridge network is backed by the docker0 interface on the host. Check its subnet:

ip -brief address show docker0
docker0          DOWN           172.17.0.1/16

The interface shows DOWN until a container is attached to it. Every container started without --network joins this default bridge and receives an address from 172.17.0.0/16.

The default bridge has one important limitation: containers on it cannot resolve each other by name. Try it:

docker run -d --name web1 nginx:alpine
docker run --rm alpine ping -c 1 web1
ping: bad address 'web1'

That is why you should create your own networks for anything beyond a single throwaway container. Remove the test container:

docker rm -f web1

Step 2 - Creating a user-defined bridge network

A user-defined bridge network provides an embedded DNS server, so containers reach each other by container name. It also isolates the containers on it from those on other networks.

Create a network with an explicit subnet. Specifying one is optional, but it avoids surprises when Docker's automatic choice overlaps with a private range you already use:

docker network create --driver bridge --subnet 172.20.0.0/24 app-net

Start an Nginx container on it, then test name resolution from a second container on the same network:

docker run -d --name web --network app-net nginx:alpine
docker run --rm --network app-net alpine ping -c 2 web
PING web (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.101 ms
64 bytes from 172.20.0.2: seq=1 ttl=64 time=0.087 ms

The name resolves. Containers use Docker's embedded DNS server at 127.0.0.11, which you can see in any container on the network:

docker run --rm --network app-net alpine cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

Fetch the Nginx page through the network to confirm connectivity at the application level:

docker run --rm --network app-net alpine wget -qO- http://web | head -4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Adding network aliases

An alias gives a container an additional DNS name on a network. Several containers can share one alias, and Docker's DNS returns all of their addresses, which gives you simple round-robin between replicas:

docker run -d --name api1 --network app-net --network-alias api nginx:alpine
docker run -d --name api2 --network app-net --network-alias api nginx:alpine
docker run --rm --network app-net alpine nslookup api

The answer lists two addresses, one per container.

Connecting a container to several networks

A container can be attached to more than one network, which is how you let a reverse proxy talk to a backend without putting every container on the same network:

docker network create frontend-net
docker network connect frontend-net web
docker inspect --format '{{range $name, $_ := .NetworkSettings.Networks}}{{$name}} {{end}}' web
app-net frontend-net

Detach it again with docker network disconnect frontend-net web.

Creating an internal network

A network created with --internal has no route to the outside world. It is useful for databases that should only be reachable by application containers:

docker network create --internal db-net
docker run --rm --network db-net alpine ping -c 1 -W 2 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
ping: sendto: Network unreachable

Step 3 - Publishing ports safely

Containers on a bridge network are not reachable from outside the host until you publish a port with -p host_port:container_port. The form of the flag decides who can connect:

docker run -d --name public-web -p 8080:80 nginx:alpine
docker run -d --name local-web -p 127.0.0.1:8081:80 nginx:alpine

List the mappings:

docker port public-web
docker port local-web
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080
80/tcp -> 127.0.0.1:8081

public-web listens on every interface, including your public IP. local-web is only reachable from the server itself, which is the right choice for services behind a reverse proxy.

Test both from the server:

curl -sI http://127.0.0.1:8080 | head -1
curl -sI http://127.0.0.1:8081 | head -1
HTTP/1.1 200 OK
HTTP/1.1 200 OK

From another machine, only http://your_server_ip:8080 answers. Clean up:

docker rm -f public-web local-web api1 api2

Step 4 - Using the host network

With --network host the container shares the host's network stack: no separate IP, no NAT, and no port mapping. A process listening on port 80 inside the container listens on port 80 of the server:

docker run -d --name host-web --network host nginx:alpine
curl -sI http://127.0.0.1 | head -1
HTTP/1.1 200 OK

Any -p flags are ignored in this mode, and the container's ports collide with services on the host. Use host networking for software that needs many dynamic ports or the lowest possible network overhead (for example some monitoring agents), not as a shortcut to avoid port mapping. It removes a layer of isolation. Remove the container:

docker rm -f host-web

Step 5 - Using the none network

The none network gives the container only a loopback interface:

docker run --rm --network none alpine ip -brief address
lo               UNKNOWN        127.0.0.1/8 ::1/128

Use it for jobs that process local files and must not send or receive traffic.

Step 6 - Creating an overlay network across two hosts

Overlay networks connect containers running on different Docker hosts through a VXLAN tunnel. They require Docker Swarm mode, even if you only use Swarm for networking.

Between the two servers, allow these ports (on the private network if you have one):

  • 2377/tcp for cluster management
  • 7946/tcp and 7946/udp for node communication
  • 4789/udp for overlay (VXLAN) traffic

On Ubuntu with UFW, run this on both servers, replacing other_server_ip with the address of the other node:

sudo ufw allow from other_server_ip to any port 2377 proto tcp
sudo ufw allow from other_server_ip to any port 7946
sudo ufw allow from other_server_ip to any port 4789 proto udp

On the first server, initialize Swarm and advertise the address the other node will use to reach it:

docker swarm init --advertise-addr your_server_ip

The output includes a docker swarm join --token ... command. Run that exact command on the second server. Then, back on the first server (the manager), confirm both nodes are ready:

docker node ls
ID                            HOSTNAME   STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
p1x7k2... *                   node1      Ready     Active         Leader           28.3.3
m9d3f5...                     node2      Ready     Active                          28.3.3

Create an attachable overlay network. --attachable lets standalone containers (not only Swarm services) join it, and --opt encrypted encrypts VXLAN traffic between nodes with IPsec:

docker network create --driver overlay --attachable --opt encrypted multi-net

Start a container on the first server:

docker run -d --name remote-web --network multi-net nginx:alpine

On the second server, reach it by name through the overlay:

docker run --rm --network multi-net alpine wget -qO- http://remote-web | grep title
<title>Welcome to nginx!</title>

The request crossed hosts through the overlay tunnel. Clean up on the first server with docker rm -f remote-web and docker network rm multi-net. If you do not need Swarm afterwards, run docker swarm leave on the second server and docker swarm leave --force on the first.

A note on macvlan

The macvlan driver gives each container its own MAC address and an IP on the physical network, so it looks like a separate machine on the LAN. It is useful on bare-metal or on-premises networks you control. On most cloud VPS platforms the upstream network only accepts traffic from the MAC address assigned to the virtual machine, so macvlan containers get no connectivity. Prefer a bridge network with published ports there.

Troubleshooting

Pool overlaps with other one on this address space. The subnet you passed to docker network create is already used by another Docker network. List subnets with docker network inspect $(docker network ls -q) --format '{{.Name}} {{range .IPAM.Config}}{{.Subnet}}{{end}}' and pick a free range.

error while removing network: network app-net has active endpoints. Containers are still attached. Find them with docker network inspect app-net --format '{{range .Containers}}{{.Name}} {{end}}', remove or disconnect them, then retry.

Containers cannot resolve each other by name. Confirm both are on the same user-defined network (not the default bridge) with docker inspect --format '{{json .NetworkSettings.Networks}}' container_name.

Containers have no internet access. Check that IP forwarding is enabled (sysctl net.ipv4.ip_forward must return 1) and that the network was not created with --internal. Restarting Docker with sudo systemctl restart docker recreates its iptables rules if something flushed them.

To remove every network not used by at least one container, run docker network prune.

Conclusion

You now know how Docker's network drivers behave: user-defined bridge networks for name-based communication on one host, careful port publishing to avoid bypassing UFW, host and none modes for special cases, and overlay networks for containers spread across several servers. As next steps, describe your networks in a compose.yaml file so they are created with your stack, put a reverse proxy on a shared network in front of your applications, and read our guide on Docker logs and troubleshooting to debug connectivity problems.