Redes Docker
https://docs.docker.com/network/
Uno de los recursos de Docker que lo hace tan poderoso es justamente la utilización de los recursos de red y cómo conectar los contenedores de forma fácil.
- Podemos conectar contenedores de máquinas diferentes
- Podemos crear una red de superposición entre varios servidores
Recursos de Linux que Docker utiliza
- veth
- es un ethernet virtual para poder crear las tarjetas de red del contenedor.
- bridge
- puente de enrutamiento del host al contenedor
- iptables
- firewall del sistema Linux
Docker ya crea algunas redes por defecto
vagrant@worker1:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
d0d374d6bfc8 bridge bridge local
51533d503c46 host host local
4a528ce35bc2 none null local
vagrant@worker1:~$
Vale la pena observar que el alcance de esta red solo pertenece al host, por eso es local. Hablaremos sobre el alcance global cuando trabajemos con clústeres y red overlay.
Por defecto, Docker no expone los puertos de sus contenedores, por lo que si levantamos un nginx, por ejemplo, quedará aislado y no podrá proporcionar el servicio.
vagrant@worker1:~$ docker container run -dit --name webserver nginx
d874bd76de6ba12e12defdd98560c871b89556a2b9f95b456a14c5a4f4508f3a
vagrant@worker1:~$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d874bd76de6b nginx "/docker-entrypoint.…" 5 seconds ago Up 4 seconds 80/tcp webserver
En este caso, solo podría acceder a ese contenedor desde dentro de otro contenedor que esté en la misma red NAT que Docker crea. Desde fuera de la NAT no sería posible.
El parámetro -p o --publish es cuando Docker publica un puerto externo que se dirige al puerto interno del contenedor.
Lo correcto sería
# puerto del host:puerto del contenedor, es decir, origen:destino
vagrant@worker1:~$ docker container run -dit -p 80:80 --name webserver nginx
51321d90ecee55960a0e965efb4c57cdbf1d4e78eb0aef7caf9f1b844d273ddc
vagrant@worker1:~$ docker container ls
# observe ahora que en ports está haciendo un mapeo
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
51321d90ecee nginx "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:80->80/tcp, :::8000->80/tcp webserver
Si accede a la IP de esta máquina, ahora devolverá la página de nginx

Controladores de red
Son los recursos que realizan la traducción de Docker a la red del host. Existen varios controladores.
Bridge
Es la red predeterminada de Docker, en caso de que no se especifique un controlador. Se crea una interfaz de red en Linux llamada dockerX, donde X es un número, y se crea una interfaz de estas para cada red bridge que se cree. No confundir con la creación de contenedores que utilizan esta red. Docker crea un veth para que cada contenedor se comunique con esta interfaz. Es una red NAT y ya tiene el recurso de DNS interno.
Para verificar la creación de la interfaz dockerX, podemos ver que tenemos una docker0 en la máquina host. Esta interfaz está DOWN porque ningún contenedor la está utilizando aún, pero quedará up tan pronto como ejecutemos el primer contenedor utilizando el bridge.
vagrant@worker1:~$ ip -c -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
enp0s3 UP 10.0.2.15/24 fe80::cd:65ff:fe0c:9769/64
enp0s8 UP 192.168.56.110/24 fe80::a00:27ff:fe4e:ae9f/64
docker0 DOWN 172.17.0.1/16 fe80::42:77ff:fe88:14cd/64
El switch (docker-proxy), la interfaz docker0 y el veth proporcionan una capa de aislamiento para el contenedor en esta figura. De esta forma es posible que varios contenedores usen sus puertos específicos, pudiendo cada contenedor, por ejemplo, utilizar el puerto 80.
El parámetro que debemos pasar es --network bridge en el comando docker container run
docker container run -dit --name webserver --network bridge -p 80:80 nginx
Verifique que la interfaz ahora quedó UP.
vagrant@worker1:~$ ip -c -br a | grep docker
docker0 UP 172.17.0.1/16 fe80::42:77ff:fe88:14cd/64
Verifique el servicio en el puerto 80 del host
vagrant@worker1:~$ sudo ss -ntpl | grep 80
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("docker-proxy",pid=2227,fd=4))
LISTEN 0 4096 [::]:80 [::]:* users:(("docker-proxy",pid=2233,fd=4))
Docker ejecuta un proceso para cada red bridge llamado docker-proxy. Docker crea reglas en iptables para el aislamiento de los procesos de red.
Podemos verificar esto a través del comando
vagrant@worker1:~$ sudo iptables -nL
Chain INPUT (policy ACCEPT)
target prot opt source destination
Chain FORWARD (policy DROP)
target prot opt source destination
# AISLAMIENTO
DOCKER-USER all -- 0.0.0.0/0 0.0.0.0/0
DOCKER-ISOLATION-STAGE-1 all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 ctstate RELATED,ESTABLISHED
DOCKER all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
Chain OUTPUT (policy ACCEPT)
target prot opt source destination
# vea esta regla
Chain DOCKER (1 references)
target prot opt source destination
ACCEPT tcp -- 0.0.0.0/0 172.17.0.2 tcp dpt:80
Chain DOCKER-ISOLATION-STAGE-1 (1 references)
target prot opt source
destination
# AISLAMIENTO
DOCKER-ISOLATION-STAGE-2 all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-ISOLATION-STAGE-2 (1 references)
target prot opt source destination
DROP all -- 0.0.0.0/0 0.0.0.0/0
RETURN all -- 0.0.0.0/0 0.0.0.0/0
Chain DOCKER-USER (1 references)
target prot opt source destination
RETURN all -- 0.0.0.0/0 0.0.0.0/0
Elimine el contenedor para la siguiente etapa.
Vamos a crear 2 contenedores y comunicarlos entre ellos por un bridge.
vagrant@worker1:~$ docker container run -dit --network bridge --name container1 --hostname servidor alpine
vagrant@worker1:~$ docker container run -dit --network bridge --name container2 --hostname cliente alpine
# CONTAINER1 con ip 172.17.0.2
vagrant@worker1:~$ docker container exec container1 ip a show eth0
21: eth0@if22: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# CONTAINER2 con ip 172.17.0.3
vagrant@worker1:~$ docker container exec container2 ip a show eth0
23: eth0@if24: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
# Haciendo ping del container1 al container2
vagrant@worker1:~$ docker container exec container1 ping -c2 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.122 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.120 ms
# Haciendo ping del container 2 al container 1
vagrant@worker1:~$ docker container exec container2 ping -c2 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.064 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.082 ms
Todo funcionando, ahora vamos a hacer ping por el hostname
vagrant@worker1:~$ docker container exec container1 ping -c2 cliente
ping: bad address 'cliente'
vagrant@worker1:~$ docker container exec container2 ping -c2 servidor
ping: bad address 'servidor'
vagrant@worker1:~$
Esto muestra que el bridge predeterminado creado por Docker no tiene resolución de DNS por la red. Este recurso no viene habilitado.
Elimine los contenedores.
Para que esto funcione necesitamos definir una red definida por el usuario (user defined network). Ventajas:
- DNS automático
- Aislamiento
- Conectar y desconectar al vuelo
- Configuración personalizada
vagrant@worker1:~$ docker network create --driver bridge --subnet 172.100.0.0/16 dockernet
8108970bf72615dcdbbe099d9419b6ac811595ee3f535e2280a18b833fb11747
vagrant@worker1:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
9efd4a6f72a7 bridge bridge local
8108970bf726 dockernet bridge local
51533d503c46 host host local
4a528ce35bc2 none null local
vagrant@worker1:~$ docker network inspect dockernet | jq
[
{
"Name": "dockernet",
"Id": "8108970bf72615dcdbbe099d9419b6ac811595ee3f535e2280a18b833fb11747",
"Created": "2022-06-28T19:55:55.045867514Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.100.0.0/16"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
# todo contenedor que se levante aparecerá aquí.
"Containers": {},
"Options": {},
"Labels": {}
}
]
Ahora vamos a crear nuevamente las máquinas y hacer ping entre ellas por el hostname y verificar.
vagrant@worker1:~$ docker container run -dit --network dockernet --name container1 --hostname server alpine
vagrant@worker1:~$ docker container run -dit --network dockernet --name container2 --hostname cliente alpine
# vamos a verificar la red
vagrant@worker1:~$ docker network inspect dockernet | jq
[
{
"Name": "dockernet",
"Id": "8108970bf72615dcdbbe099d9419b6ac811595ee3f535e2280a18b833fb11747",
"Created": "2022-06-28T19:55:55.045867514Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.100.0.0/16"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
# Observe que los contenedores están aquí
"Containers": {
"2f9b7e7b2bb80f8b1966785ddb865f858cd1e30d232410dfb212c5d798cd7a42": {
"Name": "container1",
"EndpointID": "d73ed0c87ff7370963ca166e851ebb39e96380fe0b39e1009192409728fb3b3a",
"MacAddress": "02:42:ac:64:00:02",
"IPv4Address": "172.100.0.2/16",
"IPv6Address": ""
},
"da6ac7aeb79d6c68f540588c92b585772adfb0fe04e835ef92d22d92c3a03b88": {
"Name": "container2",
"EndpointID": "2071f2e0478d8a2291c443a5be837e5150832e588f4e0fa4856dac762d7967fc",
"MacAddress": "02:42:ac:64:00:03",
"IPv4Address": "172.100.0.3/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
vagrant@worker1:~$ docker container exec container1 ping -c2 cliente
PING cliente (172.100.0.3): 56 data bytes
64 bytes from 172.100.0.3: seq=0 ttl=64 time=0.114 ms
64 bytes from 172.100.0.3: seq=1 ttl=64 time=0.090 ms
vagrant@worker1:~$ docker container exec container2 ping -c2 server
PING server (172.100.0.2): 56 data bytes
64 bytes from 172.100.0.2: seq=0 ttl=64 time=0.060 ms
64 bytes from 172.100.0.2: seq=1 ttl=64 time=0.097 ms
Ahora vamos a desconectar un contenedor y conectarlo con otra IP.
# Desconectando container 2
vagrant@worker1:~$ docker network disconnect dockernet container2
# intentando el ping y viendo que no responde
vagrant@worker1:~$ docker container exec container1 ping -c2 cliente
ping: bad address 'cliente'
# conectando el container 2 con ip .10
vagrant@worker1:~$ docker network connect --ip 172.100.0.10 dockernet container2
# verificando el cambio
vagrant@worker1:~$ docker network inspect dockernet | jq
[
{
"Name": "dockernet",
"Id": "8108970bf72615dcdbbe099d9419b6ac811595ee3f535e2280a18b833fb11747",
"Created": "2022-06-28T19:55:55.045867514Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.100.0.0/16"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"2f9b7e7b2bb80f8b1966785ddb865f858cd1e30d232410dfb212c5d798cd7a42": {
"Name": "container1",
"EndpointID": "d73ed0c87ff7370963ca166e851ebb39e96380fe0b39e1009192409728fb3b3a",
"MacAddress": "02:42:ac:64:00:02",
"IPv4Address": "172.100.0.2/16",
"IPv6Address": ""
},
"da6ac7aeb79d6c68f540588c92b585772adfb0fe04e835ef92d22d92c3a03b88": {
"Name": "container2",
"EndpointID": "2e98ca2dde0473e31014dd776d32c413fcd601b495197f0a1e0d769dbfd9b842",
"MacAddress": "02:42:ac:64:00:0a",
# con ip 10 como definimos
"IPv4Address": "172.100.0.10/16",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]
Si levantamos un contenedor y hacemos un cat en resolv.conf para verificar quién es el resolver del contenedor
vagrant@worker1:~$ docker container run -dit --name container1 alpine
vagrant@worker1:~$ docker container exec container1 cat /etc/resolv.conf
# observe que tomó el dns de la máquina
nameserver 10.0.2.3
search localdomain
vagrant@worker1:~$ docker container rm -f container1
container1
vagrant@worker1:~$ docker container run -dit --name container1 --dns 1.1.1.1 alpine
43bc80a0e5ccbf5102816bfb6d08f002bb00045532db703f60a95a9b3b288f4e
vagrant@worker1:~$ docker container exec container1 --dns 1.1.1.1 cat /etc/resolv.conf
OCI runtime exec failed: exec failed: unable to start container process: exec: "--dns": executable file not found in $PATH: unknown
vagrant@worker1:~$ docker container exec container1 cat /etc/resolv.conf
search localdomain
# ahora tomó el dns que pasamos
nameserver 1.1.1.1
vagrant@worker1:~$
Para modificar que siempre el DNS sea definido sin pasarlo, basta con alterar el daemon.json y reiniciar el servicio de Docker.
vagrant@worker1:~$ sudo vi /etc/docker/daemon.json
vagrant@worker1:~$ sudo systemctl restart docker.service -f
vagrant@worker1:~$ cat /etc/docker/daemon.json
{
"storage-driver": "overlay2",
"dns": ["8.8.8.8", "1.1.1.1"]
}
vagrant@worker1:~$ docker container run -dit --name container1 alpine
vagrant@worker1:~$ docker container exec container1 cat /etc/resolv.conf
search localdomain
nameserver 8.8.8.8
nameserver 1.1.1.1
vagrant@worker1:~$
Host
Es la red donde la IP de la máquina se comparte con el contenedor. En esta red no es posible que existan dos contenedores utilizando el mismo puerto de red. La capa de aislamiento aquí se elimina y los contenedores poseen la misma IP del host.

El parámetro que debemos pasar es --network host en el comando docker container run y no es necesario pasar el puerto. Sabrá cuál es el puerto de acuerdo con la imagen que esté ejecutando. En el Dockerfile de la imagen, el parámetro EXPOSE indicará cuál es el puerto que el contenedor necesita escuchar.
vagrant@worker1:~$ docker container run -dit --name webserver --network host nginx
vagrant@worker1:~$ docker container ls
# observe que no tiene el puerto
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
71c8dc8650ba nginx "/docker-entrypoint.…" 8 seconds ago Up 8 seconds webserver
# accediendo al propio localhost tenemos la página de nginx
vagrant@worker1:~$ curl localhost
# removido...
<title>Welcome to nginx!</title>
# removido...
# Verificando el servicio vemos que el puerto 80 va directo a nginx y no al docker-proxy
vagrant@worker1:~$ sudo ss -ntpl | grep 80
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=5029,fd=7),("nginx",pid=5028,fd=7),("nginx",pid=4981,fd=7))
LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=5029,fd=8),("nginx",pid=5028,fd=8),("nginx",pid=4981,fd=8))
# Y vemos también que no utilizó el docker0, por eso está DOWN
vagrant@worker1:~$ ip -c -br a
lo UNKNOWN 127.0.0.1/8 ::1/128
enp0s3 UP 10.0.2.15/24 fe80::cd:65ff:fe0c:9769/64
enp0s8 UP 192.168.56.110/24 fe80::a00:27ff:fe4e:ae9f/64
docker0 DOWN 172.17.0.1/16 fe80::42:77ff:fe88:14cd/64
vagrant@worker1:~$
Elimine el contenedor webserver
None
Con este parámetro el contenedor se levantará sin red, es decir, completamente aislado.
El parámetro que debemos pasar es --network none en el comando docker container run
vagrant@worker1:~$ docker container run -dit --name nonet --network none alpine ash
1e8730a7bde13e7cd4fcd1ec8dd3bc2c03397c887b4b5414745d781b41bf67f7
vagrant@worker1:~$ docker container exec nonet ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
vagrant@worker1:~$
Verifique el stack de red del contenedor ejecutando algún comando de verificación de red.
vagrant@worker1:~$ docker container run -dit --name nonet --network none alpine ash
vagrant@worker1:~$ docker container exec nonet ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
Elimine el contenedor
Macvlan
Es la red donde utilizamos los recursos de VLAN para hacer la separación lógica de la red. A través de ella podemos proporcionar direcciones IP a los contenedores que serán enrutables a la red física. También se utiliza para dar a un contenedor una IP diferente del host, pero en la misma red del host. Imaginemos que el host tiene una IP 10.0.0.10 y quiere levantar un contenedor con la IP 10.0.0.20. Entregará a su contenedor una tarjeta de red virtual, pero con una dirección MAC única. Es una de las soluciones para que un contenedor no pase por el host.

Overlay
Es una red superpuesta en la cual se crea una red lógica donde se soportan diversas redes de hosts. Este tipo de recurso se utiliza al trabajar con clústeres como es el caso de Docker Swarm y Kubernetes.
Básicamente, Docker crea un túnel por fuera de la red externa y se llama red overlay. Con esto es capaz de que todos los contenedores de todos los hosts sean accesibles por cualquiera de las IPs de los hosts. En otras palabras, cualquier IP a la que apunte será como una IP única del clúster.