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
sudoprivileges, added to thedockergroup. - 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
| Driver | Scope | Isolation | Typical use |
|---|---|---|---|
bridge | Single host | Own network namespace, NAT to the outside | Default for most containers and Compose projects |
host | Single host | None: shares the host network stack | Maximum network performance, apps that need many ports |
none | Single host | Loopback only | Batch jobs that must not touch the network |
overlay | Multi-host | VXLAN tunnel between Docker hosts | Docker Swarm services across several servers |
macvlan / ipvlan | Single host | Container gets an address on the physical LAN | Legacy 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.
WarningDocker programs its own iptables rules for published ports, and those rules are evaluated before UFW's. A port published as
-p 8080:80is reachable from the internet even ifsudo ufw statusshows it as blocked. Bind to127.0.0.1(or a private IP) for anything that should not be public.
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/tcpfor cluster management7946/tcpand7946/udpfor node communication4789/udpfor 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.
