Introducción A Docker

Almacenamiento

Los contenedores son efímeros

Los contenedores son efímeros, es decir, los ficheros, datos y configuraciones que creamos en los contenedores sobreviven a las paradas de los mismos, pero, sin embargo, son destruidos si el contenedor es destruido.

Veamos un ejemplo:

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

$ docker exec my-apache-app bash -c 'echo "<\h1\>Hola<\/h1\>" > htdocs/index.html'

$ curl http://localhost:8080
<\h1\>Hola<\/h1\>

$ docker rm -f my-apache-app
my-apache-app

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

$ curl http://localhost:8080
<\html\><\body\><\h1\>It works!<\/h1\><\/body\><\/html\>

Vemos como al eliminar el contenedor, la información que habíamos guardado en el fichero index.html se pierde, y al crear un nuevo contenedor ese fichero tendrá el contenido original.

NOTA: En la instrucción docker exec hemos ejecutado el comando con bash -c que nos permite ejecutar uno o más comandos en el contenedor de forma más compleja (por ejemplo, indicando ficheros dentro del contenedor).

Los datos en los contenedores

Ante la situación anteriormente descrita, Docker nos proporciona varias soluciones para persistir los datos de los contenedores. En este post nos vamos a centrar en las dos que considero que son más importantes:

Volúmenes docker y bind mount

Gestionando volúmenes

Algunos comando útiles para trabajar con volúmenes Docker:

Asociando almacenamiento a los contenedores

Veamos como podemos usar los volúmenes y los bind mounts en los contenedores. Para cualquiera de los dos casos lo haremos mediante el uso de dos flags de la orden docker run:

Es importante que tengamos en cuenta dos cosas importantes a la hora de realizar estas operaciones:

Trabajando con volúmenes docker:

Creamos el nuevo volumen:

javier@debian:~$ docker volume create miweb
miweb

javier@debian:~$ docker volume ls
DRIVER              VOLUME NAME
local               051b59979e0527c228be360c9b7568856a8cf37b16b9ce415f3e5fa48b812891
local               e1be424428521f02e06f73a92c2100b8cc42aaf813680bc3ee792c1353ae3abf
local               miweb

Listo.

Creamos el contenedor:

javier@debian:~$ docker pull php:7.4-apache
7.4-apache: Pulling from library/php
a076a628af6f: Already exists
02bab8795938: Already exists
657d9d2c68b9: Already exists
f47b5ee58e91: Already exists
2b62153f094c: Already exists
60b09083723b: Already exists
1701d4d0a478: Already exists
bae0c4dc63ea: Already exists
a1c05958a901: Already exists
5964d339be93: Already exists
1319bb6aacaa: Already exists
71860efe761d: Already exists
c5a84dbdd6a5: Already exists
Digest: sha256:584d2109fa4f3f0cf25358828254dc5668882167634384ad68537a3069d31652
Status: Downloaded newer image for php:7.4-apache

javier@debian:~$ docker run -d --name pruebavolumendocker -v miweb:/var/www/html -p 8080:80 php:7.4-apache
9b350c4f505b085d9633f8f46bb3a200266d4d09785c6311adae82daf1834403

Listo.

Copiamos el archivo info.php:

javier@debian:~$ docker cp info.php pruebavolumendocker:/var/www/html

Listo.

Nos dirigimos a la dirección http://localhost:8080/info.php:

Efectivamente podemos visualizar el fichero info.php.

Eliminamos el contenedor:

javier@debian:~$ docker rm -f pruebavolumendocker
pruebavolumendocker

Listo.

Creamos el contenedor:

javier@debian:~$ docker run -d --name pruebavolumendocker2 -v miweb:/var/www/html -p 8080:80 php:7.4-apache
4fe9ed47558cbc4e44c73c2d4507228828bf003048c137491df434ec6e3ca58c

Listo.

Podemos ver que sí, ya que estamos utilizando el mismo volumen.

Trabajando con bind mount:

Creamos el directorio y el fichero:

javier@debian:~$ mkdir pruebadocker

javier@debian:~$ nano pruebadocker/index.html

Listo.

Creamos el contenedor:

javier@debian:~$ docker run -d --name bindmount -v /home/javier/pruebadocker:/var/www/html -p 8080:80 php:7.4-apache
6796f397cf0f9c1331778dc917caff72885bf3e594272d46e1fa65f3b58c686f

Listo.

Nos dirigimos a la dirección http://localhost:8080:

Podemos visualizar la información.

Modificamos el contenido del fichero index.html:

javier@debian:~$ nano pruebadocker/index.html

Nos dirigimos a la dirección http://localhost:8080:

Efectivamente ha cambiado el contenido.

Eliminamos el contenedor:

javier@debian:~$ docker rm -f bindmount
bindmount

Listo.

Creamos el contenedor:

javier@debian:~$ docker run -d --name bindmount2 -v /home/javier/pruebadocker:/var/www/html -p 8080:80 php:7.4-apache
5a1d596d751ae93fb1acc99f32f830573e89652cfb5d3a4900cfc9c835ea2fdb

Listo.

Al igual que en el ejercicio anterior, podemos ver que sí, ya que estamos utilizando el mismo volumen.

Trabajando con contenedores con almacenamiento persistente

Creamos el contenedor:

javier@debian:~$ mkdir nextcloud

javier@debian:~$ docker run -d --name Nextcloud -v /home/javier/nextcloud:/var/www/html -p 8080:80 nextcloud
1fd90edb9161d28a68c58799ddeea2c58ce0acec3e85663997baae9987709274

Nos dirigimos a la dirección http://localhost:8080:

Lo instalamos con una base de datos sqlite y una vez lo tengamos instalado, subimos cualquier fichero. En mi caso he subido el fichero llamado logojp.jpg.

Listo.

Eliminamos el contenedor:

javier@debian:~$ docker rm -f Nextcloud
Nextcloud

Listo.

Creamos el nuevo contenedor llamado Nextcloud2:

javier@debian:~$ docker run -d --name Nextcloud2 -v /home/javier/nextcloud:/var/www/html -p 8080:80 nextcloud
b102fd06e36cba2e26db09414359892e3ad403a64715f7e4311cad460b2d7684

Nos dirigimos a la dirección http://localhost:8080:

Podemos ver como esta vez no nos pide instalar la aplicación, sino que directamente nos pide que iniciemos sesión. Iniciamos sesión con el usuario creado anteriormente y visualizamos los archivos:

Efectivamente se encuentra el logo que hemos subido en el anterior contenedor, por lo que no hemos perdido la información al eliminar el contenedor.

Visualizamos el contenido del directorio /home/javier/nextcloud:

javier@debian:~/nextcloud$ ls
3rdparty  config       core         data        lib           ocs           remote.php  status.php
apps      console.php  cron.php     index.html  occ           ocs-provider  resources   themes
AUTHORS   COPYING      custom_apps  index.php   ocm-provider  public.php    robots.txt  version.php

Podemos ver como efectivamente se encuentran todos los datos de la web.

Redes

Introducción a las redes en docker

Aunque hasta ahora no lo hemos tenido en cuenta, cada vez que creamos un contenedor, esté se conecta a una red virtual, yDocker 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
28: eth0@if29: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

NOTA: Hemos usado la opción --rm para que, cuando el proceso termine de ejecutarse, el contenedor se elimine.

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
...
5: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:be:71:11:9e 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:beff:fe71:119e/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: iptables -L -n y iptables -L -n - t nat y verificarlo.

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 va a permitir:

Si conectamos 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

Si probamos a acceder directamente al puerto 80 del servidor, podremos 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:

Esta red bridged, 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:

En definitiva, es importante que nuestro contenedores en producción, se estén ejecutando sobre una red definida por el usuario.

Para gestionar las redes creadas por el usuario:

NOTA: Cada red Docker que creemos, creará un puente de red específico. Podemos ver con ip a:

Asociación de redes a los contenedores

Imaginemos que hemos creado 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 ello, 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:

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 en un principio no sabemos que IP va a poseer 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

...

javier@debian:~$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED              STATUS              PORTS                NAMES
454c7149baba        wordpress           "docker-entrypoint.s…"   8 seconds ago        Up 7 seconds        0.0.0.0:80->80/tcp   servidor_wp
1923a8dc9f48        mariadb             "docker-entrypoint.s…"   About a minute ago   Up About a minute   3306/tcp             servidor_mysql

Algunas observaciones:

Para terminar, vamos a ver si realmente las configuraciones que hemos realizado mediante parámetros a la hora de crear los contenedores se han llevado a cabo.

Primeramente, vamos a comprobar en el fichero wp-config.php del contenedor de WordPress, que los parámetros de conexión a la base de datos son los mismos que los indicados en las variables de entorno.

javier@debian:~$ docker exec servidor_wp cat wp-config.php
...

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'bd_wp');

/** MySQL database username */
define( 'DB_USER', 'user_wp');

/** MySQL database password */
define( 'DB_PASSWORD', 'asdasd');

/** MySQL hostname */
define( 'DB_HOST', 'servidor_mysql');

...

Bien, vemos que sí.

Ahora vamos a comprobar que los contenedores posean conexión entre sí mediante resolución de nombres, para ello, vamos a intentar realizar un ping desde el contenedor servidor_wp usando el nombre servidor_mysql (tendremos que instalar el paquete iputils-ping en el contenedor).

javier@debian:~$ docker exec -it servidor_wp /bin/bash

root@454c7149baba:/var/www/html# apt update
...

root@454c7149baba:/var/www/html# apt install iputils-ping
...

root@454c7149baba:/var/www/html# ping servidor_mysql
PING servidor_mysql (172.18.0.2) 56(84) bytes of data.
64 bytes from servidor_mysql.red_wp (172.18.0.2): icmp_seq=1 ttl=64 time=0.127 ms
64 bytes from servidor_mysql.red_wp (172.18.0.2): icmp_seq=2 ttl=64 time=0.093 ms
64 bytes from servidor_mysql.red_wp (172.18.0.2): icmp_seq=3 ttl=64 time=0.094 ms
^C
--- servidor_mysql ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 22ms
rtt min/avg/max/mdev = 0.093/0.104/0.127/0.019 ms

Efectivamente el ping se ha realizado correctamente.

Por último, vamos a comprobar que en el fichero /etc/mysql/mariadb.conf.d/50-server.cnf del contenedor con la base de datos, se encuentre comentado el parámetro bind-address como he indicado anteriormente.

javier@debian:~$ docker exec servidor_mysql cat /etc/mysql/mariadb.conf.d/50-server.cnf
...

# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
#bind-address            = 127.0.0.1

...

En este caso la respuesta vuelve a ser afirmativa, por lo que acabamos de comprobar que todas las configuraciones se han llevado a cabo de la manera esperada.

.