Introducción

Aunque hasta ahora no lo hemos tenido en cuenta, cada vez que creamos un contenedor, esté se conecta a una red virtual y docker hace una configuración del sistema (usando interfaces puente e iptables) para que la máquina tenga una ip interna, tenga acceso al exterior, podamos mapear (DNAT) puertos,…)

$ docker run -it --rm debian bash -c "ip a"
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default 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
9: eth0@if10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

Hemos usado la opción --rm para al finalizar de ejecutar el proceso, el contenedor se elimina.

Observamos que el contenedor tiene una ip en la red 172.17.0.0/16. Además podemos comprobar que se ha creado un bridge en el host, al que se conectan los contenedores:

$ ip a
...
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:80:30:57:16 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:80ff:fe30:5716/64 scope link 
       valid_lft forever preferred_lft forever
...

Además podemos comprobar que se han creado distintas cadenas en el cortafuegos para gestionar la comunicación de los contenedores. Podemos ejecutar: sudo iptables -L -n y sudo iptables -L -n - t nat y comprobarlo.

Tipos de redes en docker

Cuando instalamos docker tenemos las siguientes redes predefinidas:

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
ec77cfd20583        bridge              bridge              local
69bb21378df5        host                host                local
089cc966eaeb        none                null                local
  • Por defecto los contenedores que creamos se conectan a la red de tipo bridge llamada bridge (por defecto el direccionamiento de esta red es 172.17.0.0/16). Los contenedores conectados a esta red que quieren exponer algún puerto al exterior tienen que usar la opción -p para mapear puertos.

    Este tipo de red nos van a permitir:

    • Aislar los distintos contenedores que tengo en distintas subredes docker, de tal manera que desde cada una de las subredes solo podremos acceder a los equipos de esa misma subred.
    • Aislar los contenedores del acceso exterior.
    • Publicar servicios que tengamos en los contenedores mediante redirecciones que docker implementará con las pertinentes reglas de iptables.

bridge1.png

  • Si conecto un contenedor a la red host, el contenedor estaría en la misma red que el host (por lo tanto toma direccionamiento del servidor DHCP de nuestra red). Además los puerto son accesibles directamente desde el host. Por ejemplo:

      $ docker run -d --name mi_servidor --network host josedom24/aplicacionweb:v1
    	    
      $ docker ps
        CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS               NAMES
        135c742af1ff        josedom24/aplicacionweb:v1   "/usr/sbin/apache2ct…"   3 seconds ago       Up 2 seconds                                  mi_servidor
    

Prueba acceder directamente al puerto 80 del servidor para ver la página web.

  • La red none no configurará ninguna IP para el contenedor y no tiene acceso a la red externa ni a otros contenedores. Tiene la dirección loopback y se puede usar para ejecutar trabajos por lotes.

Gestionando las redes en docker

Tenemos que hacer una diferenciación entre dos tipos de redes bridged:

  • La red creada por defecto por docker para que funcionen todos los contenedores.
  • Y las redes “bridged” definidas por el usuario.

Esta red “bridged” por defecto, que es la usada por defecto por los contenedores, se diferencia en varios aspectos de las redes “bridged” que creamos nosotros. Estos aspectos son los siguientes:

  • Las redes que nosotros definamos proporcionan resolución DNS entre los contenedores, cosa que la red por defecto no hace a no ser que usemos opciones que ya se consideran “deprectated” (–link).
  • Puedo conectar en caliente a los contenedores redes “bridged” definidas por el usuario. Si uso la red por defecto tengo que parar previamente el contenedor.
  • Me permite gestionar de manera más segura el aislamiento de los contenedores, ya que si no indico una red al arrancar un contenedor éste se incluye en la red por defecto donde pueden convivir servicios que no tengan nada que ver.
  • Tengo más control sobre la configuración de las redes si las defino yo.
  • Los contenedores de la red por defecto comparten todos la misma configuración de red (MTU, reglas ip tables etc…).
  • Los contenedores dentro de la red “bridge” comparten todos ciertas variables de entorno lo que puede provocar ciertos conflictos.

En definitiva: Es importante que nuestros contenedores en producción se estén ejecutando sobre una red definida por el usuario.

Para gestionar las redes creadas por el usuario:

  • docker network ls: Listado de las redes
  • docker network create: Creación de redes. Ejemplos:
    • docker network create red1
    • docker network create -d bridge --subnet 172.24.0.0./16 --gateway 172.24.0.1 red2
  • docker network rm/prune: Borrar redes. Teniendo en cuenta que no puedo borrar una red que tenga contenedores que la estén usando. deberé primero borrar los contenedores o desconectar la red.
  • docker network inspect: Nos da información de la red.

Nota: Cada red docker que creo crea un puente de red específico para cada red que podemos ver con ip a:

bridge2.png

Asociación de redes a los contenedores

Imaginemos que he creados dos redes definidas por el usuario:

$ docker network create --subnet 172.28.0.0/16 --gateway 172.28.0.1 red1
$ docker network create red2

Vamos a trabajar en un primer momento con la red1. Vamos a crear dos contenedores conectados a dicha red:

$ docker run -d --name my-apache-app --network red1 -p 8080:80 httpd:2.4

Lo primero que vamos a comprobar es la resolución DNS:

$ docker run -it --name contenedor1 --network red1 debian bash
root@98ab5a0c2f0c:/# apt update && apt install dnsutils -y
...
root@98ab5a0c2f0c:/# dig my-apache-app
...
;; ANSWER SECTION:
my-apache-app.		600	IN	A	172.28.0.2
...
;; SERVER: 127.0.0.11#53(127.0.0.11)
...

Ahora podemos probar como podemos conectar un contenedor a una red. Para conectar usaremos docker network connect y para desconectarla usaremos docker network disconnect.

$ docker network connect red2 contenedor1 

$ docker start contenedor1
contenedor1

$ docker attach contenedor1
root@98ab5a0c2f0c:/# ip a
...
46: eth0@if47: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    ...
    inet 172.28.0.4/16 brd 172.28.255.255 scope global eth0
...
48: eth1@if49: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
...    
    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth1
...

Tanto al crear un contenedor con el flag --network, como con la instrucción docker network connect, podemos usar algunos otros flags:

  • --dns: para establecer unos servidores DNS predeterminados.
  • --ip6: para establecer la dirección de red ipv6
  • --hostname o -h: para establecer el nombre de host del contenedor. Si no lo establezco será el ID del mismo.

Ejercicio: Instalación de Wordpress

Para la instalación de WordPress necesitamos dos contenedores: la base de datos (imagen mariadb) y el servidor web con la aplicación (imagen wordpress). Los dos contenedores tienen que estar en la misma red y deben tener acceso por nombres (resolución DNS) ya que de principio no sabemos que ip va a coger cada contenedor. Por lo tanto vamos a crear los contenedores en la misma red:

$ docker network create red_wp

Siguiendo la documentación de la imagen mariadb y la imagen wordpress podemos ejecutar los siguientes comandos para crear los dos contenedores:

$ docker run -d --name servidor_mysql \
             --network red_wp \
             -v /opt/mysql_wp:/var/lib/mysql \
             -e MYSQL_DATABASE=bd_wp \
             -e MYSQL_USER=user_wp \
             -e MYSQL_PASSWORD=asdasd \
             -e MYSQL_ROOT_PASSWORD=asdasd \
             mariadb
            
$ docker run -d --name servidor_wp \
             --network red_wp \
             -v /opt/wordpress:/var/www/html/wp-content \
             -e WORDPRESS_DB_HOST=servidor_mysql \
             -e WORDPRESS_DB_USER=user_wp \
             -e WORDPRESS_DB_PASSWORD=asdasd \
             -e WORDPRESS_DB_NAME=bd_wp \
             -p 80:80  
             wordpress

$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                NAMES
5b2c5a82a524        wordpress           "docker-entrypoint.s…"   9 minutes ago       Up 9 minutes        0.0.0.0:80->80/tcp   servidor_wp
f70f22aed3d1        mariadb             "docker-entrypoint.s…"   9 minutes ago       Up 9 minutes        3306/tcp             servidor_mysql

Algunas observaciones:

  • El contenedor servidor_mysql ejecuta un script docker-entrypoint.sh que es el encargado, a partir de las variables de entorno, configurar la base de datos: crea usuario, crea base de datos, cambia la contraseña del usuario root,… y termina ejecutando el servidor mariadb.

  • Al crear la imagen mariadb han tenido en cuenta de que tiene que permitir la conexión desde otra máquina, por lo que en la configuración tenemos comentado el parámetro bind-address.

  • Del mismo modo el contenedor servidor_wp ejecuta un script docker-entrypoint.sh, que entre otras cosas, a partir de las variables de entorno, ha creado el fichero wp-config.php de wordpress, por lo que durante la instalación no te ha pedido las credenciales de la base de datos.

  • Si te das cuenta la variable de entorno WORDPRESS_DB_HOST la hemos inicializado al nombre del servidor de base de datos. Como están conectada a la misma red definida por el usuario, el contenedor wordpress al intentar acceder al nombre servidor_mysql estará accediendo al contenedor de la base de datos.

  • Al servicio al que vamos a acceder desde el exterior es al servidor web, es por lo que hemos mapeado los puertos con la opción -p. Sin embargo en el contenedor de la base de datos no es necesario mapear los puertos porque no vamos a acceder a ella desde el exterior. Sin embargo, el contenedor servidor_wp puede acceder al puerto 3306 del servidor_mysql sin problemas ya que están conectados a la misma red.